diff --git a/.editorconfig b/.editorconfig index b5297ead85..87cc9903e4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,7 +22,6 @@ ktlint_code_style = intellij_idea ktlint_function_naming_ignore_when_annotated_with = Composable ktlint_standard_discouraged-comment-location = disabled ktlint_standard_function-expression-body = disabled -ktlint_standard_mixed-condition-operators = disabled ktlint_compose_lambda-param-event-trailing = disabled [*.md] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 35bc4fb816..2aabcc996f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,7 +1,7 @@ name: Bug report description: Create a report to help us address issues you are facing with the app. title: "[BUG] " -labels: [bug, 'status: needs triage'] +labels: [bug] type: Bug body: - type: markdown @@ -12,9 +12,9 @@ body: > If you wish to help triage Lawnchair's issues, visit [#4232](https://github.com/LawnchairLauncher/lawnchair/issues/4232) for more information. Thanks for taking the time to file this issue! Here are a few things to do before submitting: - 1. Use the [**latest nightly version**](https://github.com/LawnchairLauncher/lawnchair#development-builds) when reporting bugs. - 2. [**Read the FAQ**](https://lawnchair.app/faq/#common-issues) for common issues; if it is, please don't create an issue about it. - 3. Search **both [open and closed issues](https://github.com/LawnchairLauncher/lawnchair/issues?q=is%3Aissue+sort%3Aupdated-desc+)** for your bug. + 1. Make sure you're on the [latest **nightly** version](https://github.com/LawnchairLauncher/lawnchair#development-builds) of the app. + 2. [Read the FAQ](https://lawnchair.app/faq) to check if it's one of the common issues you can encounter in the launcher; if it is, please don't create an issue about it. + 3. Search through **both** [open and closed issues](https://github.com/LawnchairLauncher/lawnchair/issues?q=is%3Aissue+sort%3Aupdated-desc+) for your bug. - type: textarea id: bug-description @@ -28,11 +28,11 @@ body: id: steps-to-reproduce attributes: label: Steps to reproduce - description: Steps to reproduce the bug. **Be specific** on what steps you need to do to encounter this bug. + description: Steps to reproduce the bug. Be specific on what steps you need to do to encounter this bug. placeholder: | 1. Go to '...' 2. Click on '....' - 3. Scroll down to '...' + 3. Scroll down to '....' 4. See error validations: required: true @@ -68,12 +68,9 @@ body: id: app-version attributes: label: App version - description: | - Open Lawnchair settings > About to view the app version. - - On nightly builds, **long-press** the version name and paste the GitHub link here. + description: Open Lawnchair settings > About to view the app version. On nightly builds, long-press the version name and paste the GitHub link here. placeholder: | - 15 Beta 1 + 14 Beta 3 validations: required: true @@ -81,11 +78,7 @@ body: id: additional-context attributes: label: Additional context - description: | - Add any other context about the problem here. - - If this is a crash report, paste or link the crash message here. - placeholder: | - Crash log: https://katb.in/examplelink + description: Add any other context about the problem here. + placeholder: If this is a crash report, you can paste or link the crash message in here. validations: required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 5d643e874f..87ddd72f5d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,7 +1,7 @@ name: Feature request description: Suggest features you want the developers (or contributors) to make for the launcher. title: "[FEATURE] " -labels: [feature-request, 'status: needs triage'] +labels: [feature-request] type: Feature body: - type: markdown @@ -12,11 +12,11 @@ body: > If you wish to help triage Lawnchair's issues, visit [#4232](https://github.com/LawnchairLauncher/lawnchair/issues/4232) for more information. Thanks for taking the time to file this issue! Here are a few things to do before submitting: - 1. Use the [**latest nightly version**](https://github.com/LawnchairLauncher/lawnchair#development-builds) when requesting. - 2. [**Read the FAQ**](https://lawnchair.app/faq) to check if your request is already planned by the developers. - 3. Search **both [open and closed issues](https://github.com/LawnchairLauncher/lawnchair/issues?q=is%3Aissue+sort%3Aupdated-desc+)** for your feature request. + 1. Make sure you're on the [latest **nightly** version](https://github.com/LawnchairLauncher/lawnchair#development-builds) of the app. + 2. [Read the FAQ](https://lawnchair.app/faq) to check if it's one of the common issues you can encounter in the launcher; if it is, please don't create an issue about it. + 3. Search through **both** [open and closed issues](https://github.com/LawnchairLauncher/lawnchair/issues?q=is%3Aissue+sort%3Aupdated-desc+) for your bug. - Please keep in mind that **we may not always implement this feature request**. + Please keep in mind that we may not always implement this feature request. - type: textarea id: feature-description @@ -37,11 +37,9 @@ body: - type: dropdown id: feature-in-version-two attributes: - label: Did the feature exist in Lawnchair Legacy? + label: Did the feature exist in Lawnchair v2? (Play Store version) description: | - See [this FAQ page](https://lawnchair.app/faq/#what-happened-to-lawnchair-legacy) for more info about Lawnchair Legacy, previously - known as Lawnchair 2. - + If so, the feature might be already in the dev's to-do list and may be closed. See [this page](https://lawnchair.app/faq#can-you-return-x-feature-from-lawnchair-2) for more information. options: - "Yes" - "No" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..e3f65ef6a5 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,8 @@ +housekeeping: + - "*.md" + - .github/PULL_REQUEST_TEMPLATE/*.md + - .github/pull_request_template.md + - docs/** + +outdated: + - base-branch: '!16-dev' diff --git a/.github/release.yml b/.github/release.yml index 968b64dcc2..d15e13133a 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -3,7 +3,7 @@ changelog: labels: - bot authors: - - renovate + - renovate[bot] - lawnchair-bot - crowdin-bot categories: @@ -18,6 +18,3 @@ changelog: - title: 🧹 Housekeeping labels: - housekeeping - - title: 🧑‍💻 Dependencies - labels: - - dependencies diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 5cef531408..51645b7e00 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -6,17 +6,4 @@ labels: [ 'dependencies', ], - packageRules: [ - // Enable auto-merge for minor/patch updates. - { - matchUpdateTypes: [ - 'minor', - 'patch', - ], - automerge: true, - matchPackageNames: [ - '/.*/', - ], - }, - ], } diff --git a/.github/workflows/build_release_apk.yml b/.github/workflows/build_release_apk.yml index 585ad7927b..b3696d91c2 100644 --- a/.github/workflows/build_release_apk.yml +++ b/.github/workflows/build_release_apk.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 with: submodules: true - uses: actions/setup-java@v5 @@ -31,7 +31,7 @@ jobs: - name: Build release APK run: ./gradlew assembleLawnWithQuickstepGithubRelease bundleLawnWithQuickstepPlayRelease - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v5 with: name: Release APK path: build/outputs/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef32b63e95..9d1e68eb1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 with: submodules: true - uses: actions/setup-java@v5 @@ -51,10 +51,11 @@ jobs: echo storeFile='${{ github.workspace }}/key.jks' >> keystore.properties echo ${{ secrets.KEYSTORE }} | base64 --decode > ${{ github.workspace }}/key.jks fi + # Release variant is disabled due to build conflict | assembleLawnWithQuickstepNightlyRelease - name: Build debug APK - run: ./gradlew assembleLawnWithQuickstepGithubDebug assembleLawnWithQuickstepPlayDebug assembleLawnWithQuickstepNightlyRelease --no-configuration-cache + run: ./gradlew assembleLawnWithQuickstepGithubDebug assembleLawnWithQuickstepPlayDebug --no-configuration-cache - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v5 with: name: Debug APK path: build/outputs/apk/**/*.apk @@ -62,7 +63,7 @@ jobs: check-style: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 with: submodules: true - uses: actions/setup-java@v5 @@ -77,7 +78,7 @@ jobs: if: github.repository_owner == 'LawnchairLauncher' needs: build-debug-apk steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 with: submodules: true fetch-depth: 0 @@ -89,7 +90,7 @@ jobs: python -m pip install --upgrade pip pip install gitpython requests - name: Download artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v6 with: name: Debug APK path: artifacts/debug-apk @@ -118,13 +119,14 @@ jobs: nightly-release: runs-on: ubuntu-latest - if: github.repository_owner == 'LawnchairLauncher' && github.event_name == 'push' && github.ref == 'refs/heads/15-dev' + if: false + # if: github.repository_owner == 'LawnchairLauncher' && github.event_name == 'push' && github.ref == 'refs/heads/15-dev' needs: build-debug-apk permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: actions/download-artifact@v7 + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v6 with: name: Debug APK # Note the # and () symbols are not supported in GitHub Release filenames, even manually diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 123e8e8435..4c945dc193 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Sync Translations uses: crowdin/github-action@v2 @@ -24,7 +24,7 @@ jobs: upload_translations: false upload_sources: true download_translations: true - localization_branch_name: 15-dev-localization + localization_branch_name: 16-dev-localization create_pull_request: true base_url: 'https://lawnchair.crowdin.com' env: diff --git a/.github/workflows/crowdin_download.yml b/.github/workflows/crowdin_download.yml index 19fe5136d6..3e3d61d76d 100644 --- a/.github/workflows/crowdin_download.yml +++ b/.github/workflows/crowdin_download.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Download translations uses: crowdin/github-action@v2 @@ -26,7 +26,7 @@ jobs: upload_translations: false upload_sources: false download_translations: true - localization_branch_name: 15-dev-localization + localization_branch_name: 16-dev-localization create_pull_request: true base_url: 'https://lawnchair.crowdin.com' env: diff --git a/.github/workflows/crowdin_upload.yml b/.github/workflows/crowdin_upload.yml index f85f080306..afb14137a5 100644 --- a/.github/workflows/crowdin_upload.yml +++ b/.github/workflows/crowdin_upload.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v5 - name: Upload Strings uses: crowdin/github-action@v2 @@ -22,7 +22,7 @@ jobs: upload_translations: false upload_sources: true download_translations: false - localization_branch_name: 15-dev-localization + localization_branch_name: 16-dev-localization create_pull_request: false base_url: 'https://lawnchair.crowdin.com' env: diff --git a/.github/workflows/release_update.yml b/.github/workflows/release_update.yml index 4354f5b46d..6f24e75756 100644 --- a/.github/workflows/release_update.yml +++ b/.github/workflows/release_update.yml @@ -27,7 +27,7 @@ jobs: id-token: write attestations: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 with: submodules: true - uses: actions/setup-java@v5 @@ -55,7 +55,7 @@ jobs: with: subject-path: ${{ github.event.inputs.artifactName }} - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v5 with: name: Release APK path: ${{ github.event.inputs.artifactName }} @@ -66,9 +66,9 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 - name: Download artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v6 with: name: Release APK path: artifacts/release-apk @@ -86,9 +86,9 @@ jobs: runs-on: ubuntu-latest needs: build-release-apk steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v5 - name: Download artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v6 with: name: Release APK path: artifacts/release-apk diff --git a/.gitmodules b/.gitmodules index cf7551ac41..f2284aca65 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "platform_frameworks_libs_systemui"] path = platform_frameworks_libs_systemui url = https://github.com/LawnchairLauncher/platform_frameworks_libs_systemui + branch = 16-dev diff --git a/Android.bp b/Android.bp index eb033ee0da..07db82b10e 100644 --- a/Android.bp +++ b/Android.bp @@ -17,7 +17,19 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } -min_launcher3_sdk_version = "30" +min_launcher3_sdk_version = "31" + +// Targets that don't inherit framework aconfig libs (i.e., those that don't set +// `platform_apis: true`) must manually link them. +java_defaults { + name: "launcher-non-platform-apis-defaults", + static_libs: [ + "android.os.flags-aconfig-java", + "android.multiuser.flags-aconfig-java", + "android.appwidget.flags-aconfig-java", + "com.android.window.flags.window-aconfig-java", + ], +} // Common source files used to build launcher (java and kotlin) // All sources are split so they can be reused in many other libraries/apps in other folders @@ -31,12 +43,156 @@ filegroup { ], } +// Main Launcher source for compose, excluding the build config +filegroup { + name: "launcher-compose-enabled-src", + srcs: [ + "compose/facade/enabled/**/*.kt", + "compose/facade/core/**/*.kt", + "compose/features/**/*.kt", + ], +} + +filegroup { + name: "launcher-compose-disabled-src", + srcs: [ + "compose/facade/core/**/*.kt", + "compose/facade/disabled/**/*.kt", + ], +} + +filegroup { + name: "launcher-compose-test-helpers-src", + srcs: [ + "compose/tests/com/android/launcher3/helper/TestHelper.kt", + ], +} + +filegroup { + name: "launcher-compose-ui-tests-src", + srcs: [], // Placeholder for compose/tests/com/android/launcher3/ui/.. tests. +} + +// Tests for features that are enabled only when compose dependency is enabled & run as part of +// Launcher3Tests +filegroup { + name: "launcher-compose-unit-tests-src", + srcs: [ + "compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt", + ], +} + +filegroup { + name: "launcher-compose-tests-src", + srcs: [ + "compose/tests/**/*.kt", + ], +} + // Source code for quickstep build, on top of launcher-src filegroup { name: "launcher-quickstep-src", srcs: [ - "quickstep/src/**/*.java", "quickstep/src/**/*.kt", + "quickstep/src/**/*.java", + ], + device_common_srcs: [ + ":launcher-quickstep-processed-protolog-src", + ], +} + +filegroup { + name: "quickstep-compose-tests-src", + srcs: [ + "quickstep/compose/tests/**/*.kt", + ], +} + +// Launcher ProtoLog support +filegroup { + name: "launcher-quickstep-unprocessed-protolog-src", + srcs: [ + "quickstep/src_protolog/**/*.java", + ], +} + +java_library { + name: "launcher-quickstep_protolog-groups", + srcs: [ + "quickstep/src_protolog/**/*.java", + ], + static_libs: [ + "protolog-group", + "androidx.annotation_annotation", + "com_android_launcher3_flags_lib", + ], +} + +java_genrule { + name: "launcher-quickstep-processed-protolog-src", + srcs: [ + ":protolog-impl", + ":launcher-quickstep-unprocessed-protolog-src", + ":launcher-quickstep_protolog-groups", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) transform-protolog-calls " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.quickstep.util.QuickstepProtoLogGroup " + + "--loggroups-jar $(location :launcher-quickstep_protolog-groups) " + + "--viewer-config-file-path /system_ext/etc/launcher.quickstep.protolog.pb " + + "--output-srcjar $(out) " + + "$(locations :launcher-quickstep-unprocessed-protolog-src)", + out: ["launcher.quickstep.protolog.srcjar"], +} + +java_genrule { + name: "gen-launcher.quickstep.protolog.pb", + srcs: [ + ":launcher-quickstep-unprocessed-protolog-src", + ":launcher-quickstep_protolog-groups", + ], + tools: ["protologtool"], + cmd: "$(location protologtool) generate-viewer-config " + + "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--loggroups-class com.android.quickstep.util.QuickstepProtoLogGroup " + + "--loggroups-jar $(location :launcher-quickstep_protolog-groups) " + + "--viewer-config $(out) " + + "$(locations :launcher-quickstep-unprocessed-protolog-src)", + out: ["launcher.quickstep.protolog.pb"], +} + +prebuilt_etc { + name: "launcher.quickstep.protolog.pb", + system_ext_specific: true, + src: ":gen-launcher.quickstep.protolog.pb", + filename_from_src: true, +} + +// Source code for quickstep dagger +filegroup { + name: "launcher-quickstep-dagger", + srcs: [ + "quickstep/dagger/**/*.java", + "quickstep/dagger/**/*.kt", + ], +} + +// Source code for quickstep build with compose enabled, on top of launcher-src +filegroup { + name: "launcher-quickstep-compose-enabled-src", + srcs: [ + "quickstep/compose/facade/core/*.kt", + "quickstep/compose/facade/enabled/*.kt", + "quickstep/compose/features/**/*.kt", + ], +} + +filegroup { + name: "launcher-quickstep-compose-disabled-src", + srcs: [ + "quickstep/compose/facade/core/*.kt", + "quickstep/compose/facade/disabled/*.kt", ], } @@ -63,10 +219,130 @@ filegroup { srcs: ["proguard.flags"], } +soong_config_bool_variable { + name: "release_enable_compose_in_launcher", +} + +// Opt-in configuration for Launcher3 code depending on Jetpack Compose. +soong_config_module_type { + name: "launcher_compose_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: ["release_enable_compose_in_launcher"], + properties: [ + "srcs", + "static_libs", + ], +} + +launcher_compose_java_defaults { + name: "launcher_compose_defaults", + soong_config_variables: { + release_enable_compose_in_launcher: { + srcs: [ + ":launcher-compose-enabled-src", + ], + + static_libs: [ + "widget_picker_component", + // Compose dependencies + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + "androidx.compose.ui_ui-tooling-preview", + "androidx.compose.ui_ui-tooling", + ], + + // By default, Compose is disabled and we compile the ComposeFacade + // in compose/launcher3/facade/disabled/. + conditions_default: { + srcs: [ + ":launcher-compose-disabled-src", + ], + static_libs: [], + }, + }, + }, +} + +soong_config_module_type { + name: "launcher_compose_tests_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: ["release_enable_compose_in_launcher"], + properties: [ + "srcs", + "static_libs", + ], +} + +launcher_compose_tests_java_defaults { + name: "launcher_compose_tests_defaults", + soong_config_variables: { + release_enable_compose_in_launcher: { + srcs: [ + ":launcher-compose-test-helpers-src", + ":launcher-compose-unit-tests-src", + ], + // Compose dependencies + static_libs: [ + "widget_picker_component", + "androidx.compose.runtime_runtime", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + ], + + conditions_default: { + srcs: [], + static_libs: [], + }, + }, + }, +} + +// Opt-in configuration for Launcher Quickstep code depending on Jetpack Compose. +soong_config_module_type { + name: "quickstep_compose_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: ["release_enable_compose_in_launcher"], + properties: [ + "srcs", + "static_libs", + ], +} + +quickstep_compose_java_defaults { + name: "quickstep_compose_defaults", + soong_config_variables: { + release_enable_compose_in_launcher: { + srcs: [ + ":launcher-quickstep-compose-enabled-src", + ], + + // Compose dependencies + static_libs: [ + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + "androidx.compose.ui_ui-tooling-preview", + "androidx.compose.ui_ui-tooling", + ], + + // By default, Compose is disabled and we compile the ComposeFacade + // in compose/quickstep/facade/disabled/. + conditions_default: { + srcs: [ + ":launcher-quickstep-compose-disabled-src", + ], + static_libs: [], + }, + }, + }, +} + android_library { name: "launcher-aosp-tapl", libs: [ - "framework-statsd", + "framework-statsd.stubs.module_lib", ], static_libs: [ "androidx.annotation_annotation", @@ -76,11 +352,14 @@ android_library { "androidx.preference_preference", "SystemUISharedLib", "//frameworks/libs/systemui:animationlib", + "//frameworks/libs/systemui:contextualeducationlib", "launcher-testing-shared", ], srcs: [ "tests/tapl/**/*.java", "tests/tapl/**/*.kt", + "tests/src_tapl_build_config/**/*.java", + "tests/src_tapl_build_config/**/*.kt", ], resource_dirs: [], manifest: "tests/tapl/AndroidManifest.xml", @@ -141,7 +420,6 @@ android_library { static_libs: [ "LauncherPluginLib", "launcher_quickstep_log_protos_lite", - "android.os.flags-aconfig-java", "androidx-constraintlayout_constraintlayout", "androidx.recyclerview_recyclerview", "androidx.dynamicanimation_dynamicanimation", @@ -154,7 +432,11 @@ android_library { "//frameworks/libs/systemui:iconloader_base", "//frameworks/libs/systemui:view_capture", "//frameworks/libs/systemui:animationlib", + "//frameworks/libs/systemui:contextualeducationlib", + "//frameworks/libs/systemui:mechanics", + "//frameworks/libs/systemui:msdl", "SystemUI-statsd", + "WindowManager-Shell-shared-AOSP", "launcher-testing-shared", "androidx.lifecycle_lifecycle-common-java8", "androidx.lifecycle_lifecycle-extensions", @@ -163,8 +445,12 @@ android_library { "kotlinx_coroutines", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", - "android.appwidget.flags-aconfig-java", - "com.android.window.flags.window-aconfig-java", + "dagger2", + "jsr330", + "com_android_systemui_shared_flags_lib", + "launcher-dagger-qualifiers", + "launcher-executor-qualifiers", + "launcher-executors-module", ], manifest: "AndroidManifest-common.xml", sdk_version: "current", @@ -172,6 +458,9 @@ android_library { lint: { baseline_filename: "lint-baseline.xml", }, + flags_packages: [ + "com_android_launcher3_flags", + ], } // @@ -179,6 +468,10 @@ android_library { // android_app { name: "Launcher3", + defaults: [ + "launcher-non-platform-apis-defaults", + "launcher_compose_defaults", + ], static_libs: [ "Launcher3ResLib", @@ -190,7 +483,7 @@ android_app { ], optimize: { - proguard_flags_files: ["proguard.pro"], + proguard_flags_files: [":launcher-proguard-rules"], // Proguard is disable for testing. Derivarive prjects to keep proguard enabled enabled: false, }, @@ -198,6 +491,7 @@ android_app { sdk_version: "current", min_sdk_version: min_launcher3_sdk_version, target_sdk_version: "current", + plugins: ["dagger2-compiler"], privileged: true, system_ext_specific: true, @@ -214,8 +508,12 @@ android_app { "AndroidManifest-common.xml", ], lint: { + extra_check_modules: ["Launcher3LintChecker"], baseline_filename: "lint-baseline.xml", }, + kotlincflags: [ + "-Xjvm-default=all", + ], } // Library with all the dependencies for building quickstep @@ -226,24 +524,35 @@ android_library { "quickstep/res", ], libs: [ - "framework-statsd", + "framework-statsd.stubs.module_lib", ], static_libs: [ "Launcher3ResLib", "lottie", "SystemUISharedLib", "SettingsLibSettingsTheme", + "dagger2", + "protolog-group", + "displaylib", ], manifest: "quickstep/AndroidManifest.xml", min_sdk_version: "current", + lint: { + disabled_checks: ["MissingClass"], + }, } // Library with all the source code and dependencies for building Launcher Go android_library { name: "Launcher3GoLib", + defaults: [ + "launcher_compose_defaults", + "quickstep_compose_defaults", + ], srcs: [ ":launcher-src", ":launcher-quickstep-src", + ":launcher-quickstep-dagger", "go/quickstep/src/**/*.java", "go/quickstep/src/**/*.kt", ], @@ -258,7 +567,10 @@ android_library { "QuickstepResLib", "androidx.room_room-runtime", ], - plugins: ["androidx.room_room-compiler-plugin"], + plugins: [ + "androidx.room_room-compiler-plugin", + "dagger2-compiler", + ], manifest: "quickstep/AndroidManifest.xml", additional_manifests: [ "go/AndroidManifest.xml", @@ -267,19 +579,27 @@ android_library { min_sdk_version: "current", // TODO(b/319712088): re-enable use_resource_processor use_resource_processor: false, + kotlincflags: [ + "-Xjvm-default=all", + ], } // Library with all the source code and dependencies for building Quickstep android_library { name: "Launcher3QuickStepLib", + defaults: [ + "launcher_compose_defaults", + "quickstep_compose_defaults", + ], srcs: [ ":launcher-src", ":launcher-quickstep-src", + ":launcher-quickstep-dagger", ":launcher-build-config", ], resource_dirs: [], libs: [ - "framework-statsd", + "framework-statsd.stubs.module_lib", ], // Note the ordering here is important when it comes to resource // overriding. We want the most specific resource overrides defined @@ -291,18 +611,23 @@ android_library { ], manifest: "quickstep/AndroidManifest.xml", platform_apis: true, + plugins: ["dagger2-compiler"], min_sdk_version: "current", // TODO(b/319712088): re-enable use_resource_processor use_resource_processor: false, + kotlincflags: [ + "-Xjvm-default=all", + ], } // Build rule for Quickstep app. android_app { name: "Launcher3QuickStep", - static_libs: ["Launcher3QuickStepLib"], optimize: { - enabled: false, + proguard_flags_files: [":launcher-proguard-rules"], + enabled: true, + shrink_resources: true, }, platform_apis: true, @@ -316,7 +641,10 @@ android_app { "Launcher2", "Launcher3", ], - required: ["privapp_whitelist_com.android.launcher3"], + required: [ + "privapp_whitelist_com.android.launcher3", + "launcher.quickstep.protolog.pb", + ], resource_dirs: ["quickstep/res"], @@ -332,13 +660,11 @@ android_app { } - // Build rule for Launcher3 Go app with quickstep for Android Go devices. // Note that the following two rules are exactly same, and should // eventually be merged into a single target android_app { name: "Launcher3Go", - static_libs: ["Launcher3GoLib"], resource_dirs: [], @@ -349,6 +675,7 @@ android_app { optimize: { proguard_flags_files: ["proguard.flags"], enabled: true, + shrink_resources: true, }, privileged: true, @@ -372,9 +699,9 @@ android_app { include_filter: ["com.android.launcher3.*"], }, } + android_app { name: "Launcher3QuickStepGo", - static_libs: ["Launcher3GoLib"], resource_dirs: [], @@ -385,6 +712,7 @@ android_app { optimize: { proguard_flags_files: ["proguard.flags"], enabled: true, + shrink_resources: true, }, privileged: true, diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index e8fa644e1f..efaca3a83b 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -65,9 +65,14 @@ android:protectionLevel="signatureOrSystem" android:label="@string/permlab_write_settings" android:description="@string/permdesc_write_settings"/> + + + The content provider for exposing various launcher grid options. + The caller should hold one of the following permissions: + - android.permission.BIND_WALLPAPER + - ${applicationId}.permission.GRID_CONTROL + --> diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9e5f3e6299..7c7b2388e8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -28,6 +28,10 @@ Refer comments around specific entries on how to extend individual components. --> + + + + @@ -73,5 +78,18 @@ android:value="${packageName}.grid_control" /> + + + + + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e35466f91a..a4c7e9cea6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,10 @@ feel free to reach out on [Telegram][telegram] or [Discord][discord]. > Use the [Lawnchair Nightly builds][nightly] when reporting bugs, as your issue may have already > been fixed. +> [!WARNING] +> For [security vulnerability][security-report], please do not open an issue. Instead, follow +> the instructions in our [Security Policy][security-policy]. + - For **[bug reports][bug-reports]**, please be as detailed as possible and provide clear steps to reproduce the issue. - For **[feature requests][feature-requests]**, clearly describe the feature and its potential @@ -40,13 +44,13 @@ For translations, please visit **[Lawnchair on Crowdin][crowdin]**. 2. Open the project in Android Studio. 3. Select the `lawnWithQuickstepGithubDebug` build variant. -If you encounter errors with the `iconloaderlib` or `searchuilib` modules, run -`git submodule update --init --recursive`. +If you encounter errors with modules that ends with `lib` suffix like `iconloaderlib` or `searchuilib`, +run `git submodule update --init --recursive`. Here are some contribution tips to help you get started: - Always make sure that you're up-to-date with **Lawnchair** by setting your base branch to - `15-dev`. + `16-dev`. - Make sure your code is logical and well-formatted. If using Kotlin, see [“Coding conventions” in the Kotlin documentation][kotlin-coding-conventions]; - [The `lawnchair` package][lawnchair-package] @@ -57,10 +61,12 @@ Here are some contribution tips to help you get started: ### Additional documentation - [Lawnchair roadmap](ROADMAP.md) +- [Lawnchair verification](VERIFICATION.md) - [The Lawnchair Wiki](https://github.com/LawnchairLauncher/lawnchair/wiki) - [Lawnchair Visual Guidelines](/docs/assets/README.md) - [Lawnchair Quickstep Compat Library](compatLib/README.md) - [Lawnchair Preferences Components](lawnchair/src/app/lawnchair/ui/preferences/components/README.md) +- [Lawnchair Platform Frameworks Library SystemUI](platform_frameworks_libs_systemui/README.md) - [SystemUI Module](systemUI/README.md) - [ViewCapture](systemUI/viewcapture/README.md) - [Common](systemUI/common/README.md) @@ -69,7 +75,7 @@ Here are some contribution tips to help you get started: ### Development workflow We use a tiered workflow to balance development speed with stability. The process for merging a -change depends on its complexity and risk. All PRs should target the `15-dev` branch. +change depends on its complexity and risk. All PRs should target the `16-dev` branch. | Tier | Definition | Examples | Protocol | |-----------------------------|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -88,8 +94,7 @@ We follow the **[Conventional Commits specification][conventional-commits]**. ### Versioning scheme -As of Lawnchair 15 Beta 1, Lawnchair’s version code is composed of four parts, separated by -underscores: +Lawnchair’s version code is composed of five parts, separated by underscores:

@@ -101,39 +106,21 @@ underscores: 1. Android major version 2. Android minor version -3. Lawnchair development status +3. Lawnchair development stage 4. Lawnchair development version +5. Revision/Release number -#### Android major & minor versions +#### Lawnchair development stage -These represent the Android version in which Lawnchair is based on. -They make up the first two parts of the version code: +This table show list of development stages in use by Lawnchair: -- Major version: Indicates the main Android version. -- Minor version: Reflects any point release or update within the major version. - -Example: Android 11 will be `11_00_XX_XX` while Android 12.1 will be `12_01_XX_XX`. - -#### Development status & version - -The third and fourth parts of the version code refer to Lawnchair's development stage -and the specific version within that stage: - -- Development status: Shows the current development stage of the Lawnchair build (e.g., Alpha, - Beta). -- Development version: Specifies the incremental version within the same development stage. - -The table below shows release phase used by Lawnchair: - -| Status | Stage | -|-------------------|-------| -| Development | 00 | -| Alpha | 01 | -| Beta | 02 | -| Release Candidate | 03 | -| Release | 04 | - -Example: Alpha 5 will be `XX_XX_01_05` and Beta 3 will be `XX_XX_02_03`. +| Stage | Denote | +|-------------------|--------| +| Development | 00 | +| Alpha | 01 | +| Beta | 02 | +| Release Candidate | 03 | +| Release | 04 | ### String naming @@ -154,25 +141,33 @@ Strings `names` in `strings.xml` should follow this format: Lawnchair uses a locally stored JSON file (`google_fonts.json`) to list available fonts from Google Fonts. This file should be updated periodically or before release to include the latest fonts. -To update Lawnchair's font listing, follow these steps: +To update Lawnchair’s font listing, follow these steps: -1. Acquire +1. Get a [Google Fonts Developer API key][google-fonts-api-key]. 2. Download the JSON file from `https://www.googleapis.com/webfonts/v1/webfonts?key=API_KEY`, replacing `API_KEY` with the API key from step 1. 3. Replace the content of [`google_fonts.json`](lawnchair/assets/google_fonts.json) with the API response. +#### Writing or updating Lawnchair documentation + +Lawnchair’s documentations are written in Markdown and follow a style guides from +[Google developer documentation style guide](https://developers.google.com/style). + [telegram]: https://t.me/lccommunity [discord]: https://discord.com/invite/3x8qNWxgGZ [nightly]: https://github.com/LawnchairLauncher/lawnchair/releases/tag/nightly +[security-report]: https://github.com/LawnchairLauncher/lawnchair/security/advisories/new +[security-policy]: https://github.com/LawnchairLauncher/lawnchair/security/policy [bug-reports]: https://github.com/LawnchairLauncher/lawnchair/issues/new?assignees=&labels=bug&projects=&template=bug_report.yaml&title=%5BBUG%5D+ [feature-requests]: https://github.com/LawnchairLauncher/lawnchair/issues/new?assignees=&labels=feature%2Cenhancement&projects=&template=feature_request.yaml&title=%5BFEATURE%5D+ [code-of-conduct]: CODE_OF_CONDUCT.md [crowdin]: https://lawnchair.crowdin.com [kotlin-coding-conventions]: https://kotlinlang.org/docs/coding-conventions.html -[lawnchair-package]: https://github.com/LawnchairLauncher/lawnchair/tree/15-dev/lawnchair -[src-package]: https://github.com/LawnchairLauncher/lawnchair/tree/15-dev/src +[lawnchair-package]: https://github.com/LawnchairLauncher/lawnchair/tree/16-dev/lawnchair +[src-package]: https://github.com/LawnchairLauncher/lawnchair/tree/16-dev/src [conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0/ [google-fonts-api-key]: https://developers.google.com/fonts/docs/developer_api#APIKey + diff --git a/GITHUB_CHANGELOG.md b/GITHUB_CHANGELOG.md index 56b9f5ad61..1924b17439 100644 --- a/GITHUB_CHANGELOG.md +++ b/GITHUB_CHANGELOG.md @@ -1,29 +1,247 @@ -Lawnchair 15 Beta 2 is now available! This is a stability-focused release that addresses numerous bugs from Beta 1, while adding a few quality of life improvements. +Lawnchair 16 pE Development 2 is here! Contributors are encouraged to target this branch instead of +older (i.e., Lawnchair `15-dev`). -Note that this release only supports QuickSwitch from Android 10 to Android 15 QPR0 (initial release). +### 🏗️ Development 3 (Draft) -This release includes the following improvements: -* Redesigned the launcher search backend infrastructure, with a cleaner UI for settings - * Replace Mull search provider with IronFox (#5780) - * Fix launch intent for Kagi search provider (#5800) -* Added support for infinite scrolling in home screen (#5807); -* Added support for re-ordering apps inside app drawer folders (via settings) (#6173) -* Added an option in Settings to clear all items from the home screen (#6125) -* Added an option to turn off the alpha change behind the search bar when scrolling in the app drawer (#5934) -* Improved the layout of the About screen, and added a changelog viewer for Nightly builds (##5711) +This release have been tested with: +* ☁️ Pixel 6 (Android 12.0) +* 📱 Nothing (3a)-series (Android 15, Android 16.0) +* 📱 Vivo Y21 (Android 12.0) +* 📱 HTC Wildfire E3 lite (Android 12.0) -Alongside that, several issues have also been fixed: -* Fixed an issue that caused the search widget to fail to launch the Google app on Android 14 and above. -* Addressed multiple crashes on older Android versions. -* Resolved a race condition that could cause a crash when customizing an app that was being uninstalled. -* Corrected various visual bugs, including: - * "Customize" bottom sheet not updating state - * Search state would not clear when app drawer is hidden (#5933) - * Wrong icon scaling in app drawer when home screen icons are resized (#5932) - * Home screen page indicator being partially displayed (#5937) +Compatibility list: -Alongside that, this release contains the usual performance improvements, miscellaneous bug fixes, dependency updates, and new bugs & translations. +| 🏗️ Crash | 💫 Limited features | 🥞 Fully supported | +|-------------|---------------------|--------------------| +| Android 8.1 | | Android 12.0 | +| Android 9 | | Android 12.1 | +| Android 10 | | Android 13 | +| Android 11 | | Android 14 | +| | | Android 15 | +| | | Android 16 | -Thanks as well to our new contributors: @lebao3105, @IzzySoft, @foXaCe, @itsaky, @victor-marino, @garghimanshu0786, @VBansal99, @Chaikew, and @abhixv +> [!NOTE] +> QuickSwitch compatibility have not been tested at any time during the development of Bubble Tea! -[Support Lawnchair's development by donating on Open Collective](https://opencollective.com/lawnchair) +#### Features +* [Lawnchair] Complex Clover icon shape +* [Lawnchair] Very Sunny icon shape +* [Lawnchair/Font] Update Google Fonts listing to 25102025 + +#### Fixes +* Disable OEM override on launcher settings, (reimplement `ENABLE_AUTO_INSTALLS_LAYOUT` | c51b2a221838aefb610b7146fc4ef7cb34e5e495) +* [Lawnchair/Iconloaderlib] Reimplement custom app name +* [Lawnchair] Reimplement Launcher3 debug page +* [Lawnchair] Reimplement Caddy and App drawer folder +* [Lawnchair] Reimplement Hotseat toggle +* [Lawnchair] Reimplement Favorite application label +* [Lawnchair] Hotseat positioning with favorite icon label enabled placed the same even if label is disabled +* [Lawnchair] Hotseat background now have a reasonably sized margin compared to D2 +* [Lawnchair] Qsb sizing now correctly estimate the width based on width of the app/widget layout or DeviceProfile on device with inlined Qsb +* [Lawnchair] Reimplement Allapps opacity configuration +* [DeviceProfile] Crash from createWindowContext on less than Android 12.0 +* [QuickstepLauncher] Ignore trying to set SystemUiProxy icon sizes on less than Android 12.1 +* [Lawnchair/BlankActivity] Apply Material 3 Expressive button animations +* [Launcher] Disable add widget button if home screen is locked +* [Lawnchair/Iconloaderlib] Crash when trying to set `null` monochrome icon on less than Android 12.1 +* [SystemUI/Unfold] Crash when getting configuration for foldable-specific resources +* [Lawnchair/Iconloaderlib] Don't parse monochrome drawable in Android 12.1 or less +* [Launcher3/AllApps] Allow theming of Expressive allapps +* [Lawnchair] Lawnchair can now be compiled in release mode + +### 🥞 Development 2 + +Originally going to launch D2 if most of the comestic bug fixes have been resolved, but hit a +stability milestone instead. + +This release includes 15 new features, and 20 bug fixes, +Lawnchair settings now takes shape of initial material 3 expressive redesign, [(but by no mean finish!)][Lawnget] +launcher should now render icons better than D1 milestone, with auto-adaptive icons feature reimplemented. + +This release have been tested with: +* ☁️ Pixel 6 (Android 12.0) - Build: Ad-hoc +* ☁️ Pixel 6a (Android 12.1) - Build: Ad-hoc +* ☁️ Pixel 7 (Android 13) - Build: Ad-hoc +* ☁️ Pixel 9 (Android 15, Android 16.0) - Build: Ad-hoc +* ☁️ Pixel 9 Pro Fold (Android 14, Android 15) - Build: Ad-hoc +* ☁️ Vivo V40 (Android 15) - Build: Ad-hoc +* ☁️ Xiaomi MIX (Android 15) - Build: Ad-hoc +* 📱 Nothing (3a)-series (Android 15) - Build: pE-`15102025` +* 📱 Pixel 9 Pro XL (Android 16.0 QPR2 Beta 2) - Build: pE-`02102025` +* 📱 BLU View 5 Pro (Android 14) - Build: pE-`02102025` +* 📱🔥 Vivo Y21 (Android 12.0) - Build: pE-`08102025` + +> [!NOTE] +> QuickSwitch compatibility have not been tested at any time during the development of Bubble Tea! + +[Lawnget]: https://www.google.com/teapot + +Compatibility list: + +| 🏗️ Crash | 💫 Limited features | 🥞 Fully supported | +|-------------|---------------------|--------------------| +| Android 8.1 | Android 12.0 | Android 12.1 | +| Android 9 | | Android 13 | +| Android 10 | | Android 14 | +| Android 11 | | Android 15 | +| | | Android 16 | + +#### Features + +* Enable All Apps Blur Flags on Phone (oops, forgot about the allAppsSheetForHandheld flag) +* Make Safe Mode check more reliable +* Smartspace Battery now reports battery charging status of Fast (more than 90% of 20 W) and Slow (less than 90% of 5 W) charging +* Show pseudonym version to Settings +* Resizing workspace calculate items position more accurately +* Update Lawnchair default grid size to 4×7 (or 4×6 with smartspace widget) +* Reimplement Hotseat background customisation +* Make haptic on a locked workspace use Google MSDL vibration +* Make Launcher3 colour more accurate to upstream Android 16 +* ProvideComposeSheetHandler now have expressive blur +* Lawnchair Settings now uses Material 3 Expressive +* Animate keyboard on/off state on app drawer search (Try enabling automatically show keyboard in app drawer settings and swipe up and down or directly tap “Apps list” in popup menu) -> (Backport not possible) +* Add LeakCanary check to all debug variant of the application +* [DEBUG] Launcher3 feature status diagnostic check in debug menu +* [Documentation] Add more visibility into both app certificate and SLSA verification for app authenticity check [VERIFICATION.md](VERIFICATION.md) +* [Documentation] Initial drafting of Improve documentation v6 (pave-path) +* [Launcher] Widget animations during resize +* [Iconloaderlib] Enable second hand for the clock app + +#### Fixes + +* Fix unable to access preview for icon style +* Popup's Arrow Theme now has the correct theme +* Widget should open normally after a workaround (C7evQZDJ) +* Fix (1) Search bar and Dock, (2) Folders and App Drawer settings didn't open due to init problems +* Lawnchair should hopefully remember what grid they should be using +* Most if not all of Lawnchair settings should be usable without crashes +* Correct Baseline Profile from old `market` to `play` variant, and now should calculate profile for `nightly` +* Fix Private Space crash when Lawnchair is set as Launcher due to flags only available on A16 +* Fix crash on a device with strict export receiver requirements on A14 +* Interactable widget crashing due to App Transition Manager being null (C7evQZDJ) +* Icon not responding to mouse cursor -> (Backported to Lawnchair 15) +* Rare NoSuchMethodError crash on IMS canImeRenderGesturalNavButtons +* [Lawnchair] Reimplement Bulk icons toggle +* SettingsCache crashing with SecurityException with unreadable keys (@hide) in Android 12 and newer (assume false) +* Assume flags `enableMovingContentIntoPrivateSpace` is false when ClassNotFoundException on Android 16 devices +* Rare NoSuchMethodError crash on SurfaceControl setEarlyWakeupStart and setEarlyWakeupEnd +* Properly align built-in smartspace in workspace +* Use WM Proxy from Lawnchair instead of System, fix Android 8.1/9/10/11/12.0/12.1 regarding SE, NSME like SystemBarUtils -> (dWkyIGw9), (reworked CllOXHJv) + * LawnchairWindowManagerProxy have been migrated to Dagger + * SystemWindowManagerProxy have been left unused +* [Lawnchair/Iconloaderlib] Update CustomAdaptiveIconDrawable to latest AOSP 13 +* [Iconloaderlib] Reset most of the changes to favour more AOSP 16_r02 code then Lawnchair (need rewrite) + * fix icon loaded in monochrome and always monochrome when it is not supposed to + * fix notification dots being twice the size with notification count +* [Lawnchair/Iconloaderlib] Reimplement Lawnchair Iconloaderlib (adaptive icons, monochrome, regular icon) + +#### Known Bugs +* Preview can't show device wallpaper -> (lIxkAYGg) +* IDP Preview doesn't refresh on settings change -> workaround is to hit apply and re-open the preview -> (ZbLX3438) +* Workspace theme doesn't refresh until restart -> (ZbLX3438) -> Fixed as part of (31lLEflf, 1MevNrzp) +* Lawnchair Colour can't handle restart causing default colour to be used instead -> Fixed? -> Properly fixed as part of (31lLEflf, 1MevNrzp) +* (Investigating) Work profile switch on widget selector *may* have reverted to Lawnchair 15 style +* Full lists: https://trello.com/b/8IdvO81K/pe-lawnchair + +### Development 1 + +First development milestone! Basic launcher functionality should be stable enough. + +* Make Lawnchair Launcher launchable in Android 12.1, 13, 14, 15, 16 +* Remove two deprecated features (Use Material U Popup, and Use dot pagination) +* Add pseudonym version in debug settings +* Adapt Lawnchair code to Launcher3 16 +* Make basic features of Launcher work (App Drawer, Home Screen, Search, Folders, Widgets) +* Enable Material Expressive Flags (Try swiping through launcher page) +* Enable All Apps Blur Flags (Try opening All Apps on supported devices) +* Enable MSDL Haptics Feedback Flags (Try gliding widget or icons across the homescreen) +* Make Predictive Back Gesture work on Android 13, 14, 15, 16 (Try swiping left or right on gesture-based navigational) +* Programmatically set Safe Mode status + +#### Known Bugs + +* App Icon may sometimes render with less than 0 in height/width causing blank icon to be rendered and crashing ISE on customising icons -> (31lLEflf) +* Any Lawnchair settings using IDP will crash the launcher -> Fixed in Lawnchair 16 pE Development 2 +* Icon pack isn't usable -> (DXo69Qzd) +* Dynamic icons will not be themed by launcher +* Full lists: https://trello.com/b/8IdvO81K/pe-lawnchair + +### Snapshot 6 + +This is a developer-focused change log: + +This snapshot marks the first time Lawnchair 16 is able to compile and build an APK! + +* Fix all issues with Java files in both `lawn` and `src` +* Make Lawnchair compilable (with instant crash) +* Move to KSP for Dagger code generation + +### Snapshot 5 + +This is a developer-focused change log: + +This snapshot now able to compile all sources (Kotlin files only) + +* Fix MORE MORE MORE `lawn` issues +* Use Gradle Version Catalog for consistent dependency version across all modules (Full implementation @ LawnchairLauncher/Lawnchair#5753) +* Magically fix ASM Instrumentation issues (I didn't do anything, it just works now) +* Fix ALL the issues in kotlin stage (`compileLawnWithQuickstepNightlyDebugKotlin`) +* Reintroduce some features from Lawnchair +* Add compatibility checks and workarounds for them +* Fix most issues with Java files in both `lawn` and `src` + +### Snapshot 4 + +This is a developer-focused change log: + +This snapshot marks the first time Lawnchair 16 is able to compile all Launcher3 sources! + +* Add `MSDLLib` to `platform_frameworks_libs_systemui` +* Add `contextualeducationlib` to `platform_frameworks_libs_systemui` +* Fix issues in both `lawn` and `src` modules +* Fix AIDL sources +* Resolve Lawnchair/LC-TODO lists +* Merge `wmshell.shared` res with res from `wmshell` +* Consistent build reproducibility by specifying dependencies in `build.gradle` +* Some ASM Instrumentation issues (and re-add some…) +* Update documentations + +### Snapshot 3 + +This is a developer-focused change log: + +Not a lot of errors left to go! + +* Finish correctly implementing all Dagger functions (?) +* Merge Lawnchair 15 Beta 1 into Bubble Tea + * Support for 16-kb page size devices +* Repository rebased and dropped commit + * Switch back from turbine-combined variant to javac variant for prebuilt SystemUI-core-16 because issues with LFS + * MORE MORE fixes regarding turbine-combined to javac +* Publish `platform_frameworks_libs_systemui` to pe 16-dev branch +* ATLEAST check to almost every launcher3 source file +* `Utils` module (stripped) +* Fix Dagger duplicated classes (because of Dagger dependency ksp/kapt mixing) +* Build reproducibility improvements by specifying dependencies in `build.gradle` files +* Fix some of the issues in both `lawn` and `src` modules + +### Snapshot 2 + +This is a developer-focused change log: + +This snapshot milestone marked the first time Lawnchair now able to compile all supplementary +modules, `src` + `lawn` will be in Snapshot 5 or Development 1 milestone. + +* Merge flags +* Fix some issues with launcher3 sources. +* A temporary workaround with framworks.jar not adding in anim module. +* Shared not having access to animationlib. +* **Switch from javac variant to turbine-combined variant for prebuilt SystemUI-core-16**. + +### From Initial snapshot 0 and 1 + +This is a developer-focused change log: +* Prebuilt updated to Android 16-0.0_r2 (Android 16.0.0 Release 2) +* Submodule have also been refreshed to A16r2 +* Baklava Compatlib (QuickSwitch compatibility not guaranteed) +* Refreshed internal documentation like prebuilt, systemUI diff --git a/OWNERS b/OWNERS index a66bf54b58..65a5cf0841 100644 --- a/OWNERS +++ b/OWNERS @@ -6,11 +6,8 @@ adamcohen@google.com hyunyoungs@google.com -twickham@google.com vadimt@google.com winsonc@google.com -jonmiranda@google.com -awickham@google.com agvard@google.com # Launcher workspace eng team @@ -23,19 +20,22 @@ fransebas@google.com pinyaoting@google.com andonian@google.com sihua@google.com +abegovic@google.com # Multitasking eng team tracyzhou@google.com peanutbutter@google.com jeremysim@google.com atsjenk@google.com -brianji@google.com +hwwang@google.com # Overview eng team alexchau@google.com samcackett@google.com silvajordan@google.com uwaisashraf@google.com +vinayjoglekar@google.com +willosborn@google.com # Physical Keyboard & Trackpad eng team patmanning@google.com @@ -45,11 +45,34 @@ helencheuk@google.com shamalip@google.com zakcohen@google.com +# System Navigation team +brianji@google.com +jonmiranda@google.com +jagrutdesai@google.com +randypfohl@google.com +saumyaprakash@google.com +twickham@google.com +victortulias@google.com + +## Note: some of the below overlap and also work on other integrations like Circle to Search. + +# All Apps / QSB team +awickham@google.com +brdayauon@google.com +ganjam@google.com +kylim@google.com + +# Smartspace team +xilei@google.com +davidct@google.com +iamiam@google.com +jiuyu@google.com + per-file FeatureFlags.java, globs = set noparent -per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com +per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com, abegovic@google.com per-file DeviceConfigWrapper.java, globs = set noparent -per-file DeviceConfigWrapper.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com +per-file DeviceConfigWrapper.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, abegovic@google.com # Predictive Back -per-file LauncherBackAnimationController.java = shanh@google.com, gallmann@google.com \ No newline at end of file +per-file LauncherBackAnimationController.java = shanh@google.com, gallmann@google.com diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 9051ca8562..f99e9ca556 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -1,13 +1,14 @@ [Builtin Hooks] ktfmt = true +bpfmt = true [Builtin Hooks Options] ktfmt = --kotlinlang-style +bpfmt = -d [Tool Paths] ktfmt = ${REPO_ROOT}/external/ktfmt/ktfmt.sh [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT} - -flag_hook = ${REPO_ROOT}/frameworks/base/packages/SystemUI/flag_check.py --msg=${PREUPLOAD_COMMIT_MESSAGE} --files=${PREUPLOAD_FILES} --project=${REPO_PATH} +alint_hook = ${REPO_ROOT}/vendor/google/tools/alint diff --git a/README.md b/README.md index 694336fea5..1c651f2780 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Lawnchair 15 +# Lawnchair 16 [![Build debug APK](https://github.com/LawnchairLauncher/lawnchair/actions/workflows/ci.yml/badge.svg)](https://github.com/LawnchairLauncher/lawnchair/actions/workflows/ci.yml) [![Build release APK](https://github.com/LawnchairLauncher/lawnchair/actions/workflows/release_update.yml/badge.svg)](https://github.com/LawnchairLauncher/lawnchair/actions/workflows/release_update.yml) @@ -9,25 +9,31 @@ [![GitHub Downloads](https://img.shields.io/github/downloads/LawnchairLauncher/lawnchair/total.svg?label=GitHub%20Downloads&logo=github)](https://github.com/LawnchairLauncher/lawnchair/releases) [![Play Store Installs](https://img.shields.io/endpoint?color=green&logo=googleplay&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.lawnchair.play%26l%3DPlay%2520Store%2520Installs%26m%3D%24shortinstalls)](https://play.google.com/store/apps/details?id=app.lawnchair.play) +> [!WARNING] +> This branch contains major changes from the rebase of Launcher3, including breaking changes and refactors that can cause Lawnchair to break. +> +> If you wish to contribute, read our [contributing guidelines](CONTRIBUTING.md). Note that this branch will undergo many changes as we slowly refactor our codebase, so the `16-dev` branch may be particularly unfriendly to new contributors. It is still possible to submit changes to `15-dev`, but new feature development will be focused on this branch. +> +> For regular users, we recommend staying on `15-dev` for stability purposes. + - - - - Google Pixel running Lawnchair Launcher with green wallpaper + + A device running Lawnchair Launcher with green flower wallpaper Lawnchair is a free, open-source home app for Android. Taking Launcher3—Android’s default home app—as a starting point, it ports Pixel Launcher features and introduces rich customization options. -This branch houses the codebase of Lawnchair 15, which is currently in beta and is based on Launcher3 from Android 15. For Lawnchair 9 to 14, see the branches with the `9-` to `14-` prefixes, respectively. +This branch houses the codebase of Lawnchair 16, which is currently in development and is based on Launcher3 from Android 16. For Lawnchair 9 to 15, see the branches with the `9-` to `15-` prefixes, respectively. ## Features -- **Material You Theming:** Adapts to your wallpaper and system theme. +- **Material Expressive Theming:** Adapts to your wallpaper and system theme. - **At a Glance Widget:** Displays information *at a glance* with support for [Smartspacer](https://github.com/KieronQuinn/Smartspacer). -- **QuickSwitch Support:** Integrates with Android Recents on Android 10 and newer. (requires root) +- **QuickSwitch Support:** Integrates with Android Recents on Android 10-15. (requires root) - **Global Search:** Allows quick access to apps, contacts, and web results from the home screen. - **Customization Options:** Provides options to tweak icons, fonts, and colors to your liking. - And more! @@ -38,26 +44,26 @@ This branch houses the codebase of Lawnchair 15, which is currently in beta and - - Get it on Google Play + + Get it on Google Play - - Get it on IzzyOnDroid + + Get it on IzzyOnDroid - - Get it on Obtainium + + Get it on Obtainium - - Get it on GitHub + + Get it on GitHub

@@ -74,21 +80,11 @@ These builds offer the latest features and bug fixes at a cost of being slower a ### Verification -Verify the integrity of your Lawnchair download using these SHA-256 hashes: - -###### Google Play -``` -47:AC:92:63:1C:60:35:13:CC:8D:26:DD:9C:FF:E0:71:9A:8B:36:55:44:DC:CE:C2:09:58:24:EC:25:61:20:A7 -``` - -###### Elsewhere -``` -74:7C:36:45:B3:57:25:8B:2E:23:E8:51:E5:3C:96:74:7F:E0:AD:D0:07:E5:BA:2C:D9:7E:8C:85:57:2E:4D:C5 -``` +Please visit [Lawnchair Verification](VERIFICATION.md) on way to verify Lawnchair. ## Contributing -Please visit the [Lawnchair Contributing Guidelines](CONTRIBUTING.md) for information and tips on contributing to Lawnchair. +Please visit the [Lawnchair contributing guidelines](CONTRIBUTING.md) for information and tips on contributing to Lawnchair. ## Supporting Lawnchair diff --git a/SECURITY.md b/SECURITY.md index 9aa0b671f6..430d3a7682 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,8 +9,9 @@ The latest version of Lawnchair is the only supported version. | Version | Supported | -| -------------- | ------------------ | +|----------------|--------------------| | Nightly build | :white_check_mark: | +| 16 | :white_check_mark: | | 15 | :white_check_mark: | | 14 | :x: | | 13 | :x: | diff --git a/TELEGRAM_CHANGELOG.txt b/TELEGRAM_CHANGELOG.txt index b26df88989..104b91b32e 100644 --- a/TELEGRAM_CHANGELOG.txt +++ b/TELEGRAM_CHANGELOG.txt @@ -1,18 +1,197 @@ -Hey everyone, and happy holidays from the Lawnchair team! 🎄 +Lawnchair 16 pE Development 2 is here! Contributors are encouraged to target this branch instead of +older (i.e., Lawnchair `15-dev`). -We're excited to announce that Lawnchair 15 Beta 2 is now available for download! +### Development 2 -This release contains several bug fixes and quality-of-life improvements that make the Lawnchair 15 experience much more stable and enjoyable. Some of the highlights include: +Originally going to launch D2 if comestic bug fixes have been resolved, but hit a +stability milestone instead. -- A completely revamped search experience -- Infinite home screen scrolling -- The ability to reorder apps inside app drawer folders -- ...and many more fixes for crashes and visual bugs! +This release includes 15 new features, and 20 bug fixes, +Lawnchair settings now takes shape of initial material 3 expressive redesign, [(but by no mean finish!)](https://www.google.com/teapot) +launcher should now render icons better than D1 milestone, with auto-adaptive icons feature reimplemented. -A huge thank you to everyone who made this release possible: our code contributors on GitHub, our translators on Crowdin, and our financial supporters on Open Collective. Your support is what keeps this project going. +This release have been tested with: +* ☁️ Pixel 6 (Android 12.0) - Build: Ad-hoc +* ☁️ Pixel 6a (Android 12.1) - Build: Ad-hoc +* ☁️ Pixel 7 (Android 13) - Build: Ad-hoc +* ☁️ Pixel 9 (Android 15, Android 16.0) - Build: Ad-hoc +* ☁️ Pixel 9 Pro Fold (Android 14, Android 15) - Build: Ad-hoc +* ☁️ Vivo V40 (Android 15) - Build: Ad-hoc +* ☁️ Xiaomi MIX (Android 15) - Build: Ad-hoc +* 📱 Nothing (3a)-series (Android 15) - Build: pE-`15102025` +* 📱 Pixel 9 Pro XL (Android 16.0 QPR2 Beta 2) - Build: pE-`02102025` +* 📱 BLU View 5 Pro (Android 14) - Build: pE-`02102025` +* 📱🔥 Vivo Y21 (Android 12.0) - Build: pE-`08102025` -We'll have a full Lawnstate blog post coming soon with a look back at the year and a deep dive into the ongoing work for Lawnchair 16. +> [!NOTE] +> QuickSwitch compatibility have not been tested at any time during the development of Bubble Tea! -For now, enjoy a more stable home screen for the holidays! +Compatibility list: -Download and changelog: https://github.com/LawnchairLauncher/lawnchair/releases/tag/v15.0.0-beta2 +| 🏗️ Crash | 💫 Limited features | 🥞 Fully supported | +|-------------|---------------------|--------------------| +| Android 8.1 | Android 12.0 | Android 12.1 | +| Android 9 | | Android 13 | +| Android 10 | | Android 14 | +| Android 11 | | Android 15 | +| | | Android 16 | + +#### Features + +* Enable All Apps Blur Flags on Phone (oops, forgot about the allAppsSheetForHandheld flag) +* Make Safe Mode check more reliable +* Smartspace Battery now reports battery charging status of Fast (more than 90% of 20 W) and Slow (less than 90% of 5 W) charging +* Show pseudonym version to Settings +* Resizing workspace calculate items position more accurately +* Update Lawnchair default grid size to 4×7 (or 4×6 with smartspace widget) +* Reimplement Hotseat background customisation +* Make haptic on a locked workspace use Google MSDL vibration +* Make Launcher3 colour more accurate to upstream Android 16 +* ProvideComposeSheetHandler now have expressive blur +* Lawnchair Settings now uses Material 3 Expressive +* Animate keyboard on/off state on app drawer search (Try enabling automatically show keyboard in app drawer settings and swipe up and down or directly tap “Apps list” in popup menu) -> (Backport not possible) +* Add LeakCanary check to all debug variant of the application +* [DEBUG] Launcher3 feature status diagnostic check in debug menu +* [Documentation] Add more visibility into both app certificate and SLSA verification for app authenticity check [VERIFICATION.md](VERIFICATION.md) +* [Documentation] Initial drafting of Improve documentation v6 (pave-path) +* [Launcher] Widget animations during resize +* [Iconloaderlib] Enable second hand for the clock app + +#### Fixes + +* Fix unable to access preview for icon style +* Popup's Arrow Theme now has the correct theme +* Widget should open normally after a workaround (C7evQZDJ) +* Fix (1) Search bar and Dock, (2) Folders and App Drawer settings didn't open due to init problems +* Lawnchair should hopefully remember what grid they should be using +* Most if not all of Lawnchair settings should be usable without crashes +* Correct Baseline Profile from old `market` to `play` variant, and now should calculate profile for `nightly` +* Fix Private Space crash when Lawnchair is set as Launcher due to flags only available on A16 +* Fix crash on a device with strict export receiver requirements on A14 +* Interactable widget crashing due to App Transition Manager being null (C7evQZDJ) +* Icon not responding to mouse cursor -> (Backported to Lawnchair 15) +* Rare NoSuchMethodError crash on IMS canImeRenderGesturalNavButtons +* [Lawnchair] Reimplement Bulk icons toggle +* SettingsCache crashing with SecurityException with unreadable keys (@hide) in Android 12 and newer (assume false) +* Assume flags `enableMovingContentIntoPrivateSpace` is false when ClassNotFoundException on Android 16 devices +* Rare NoSuchMethodError crash on SurfaceControl setEarlyWakeupStart and setEarlyWakeupEnd +* Properly align built-in smartspace in workspace +* Use WM Proxy from Lawnchair instead of System, fix Android 8.1/9/10/11/12.0/12.1 regarding SE, NSME like SystemBarUtils -> (dWkyIGw9), (reworked CllOXHJv) + * LawnchairWindowManagerProxy have been migrated to Dagger + * SystemWindowManagerProxy have been left unused +* [Lawnchair/Iconloaderlib] Update CustomAdaptiveIconDrawable to latest AOSP 13 +* [Iconloaderlib] Reset most of the changes to favour more AOSP 16_r02 code then Lawnchair (need rewrite) + * fix icon loaded in monochrome and always monochrome when it is not supposed to + * fix notification dots being twice the size with notification count +* [Lawnchair/Iconloaderlib] Reimplement Lawnchair Iconloaderlib (adaptive icons, monochrome, regular icon) + +#### Known Bugs +* Preview can't show device wallpaper -> (lIxkAYGg) +* IDP Preview doesn't refresh on settings change -> workaround is to hit apply and re-open the preview -> (ZbLX3438) +* Workspace theme doesn't refresh until restart -> (ZbLX3438) -> Fixed as part of (31lLEflf, 1MevNrzp) +* Lawnchair Colour can't handle restart causing default colour to be used instead -> Fixed? -> Properly fixed as part of (31lLEflf, 1MevNrzp) +* (Investigating) Work profile switch on widget selector *may* have reverted to Lawnchair 15 style +* Full lists: https://trello.com/b/8IdvO81K/pe-lawnchair + +### 🥞 Development 1 + +First development milestone! Basic launcher functionality should be stable enough. + +* Make Lawnchair Launcher launchable in Android 12.1, 13, 14, 15, 16 +* Remove two deprecated features (Use Material U Popup, and Use dot pagination) +* Add pseudonym version in debug settings +* Adapt Lawnchair code to Launcher3 16 +* Make basic features of Launcher work (App Drawer, Home Screen, Search, Folders, Widgets) +* Enable Material Expressive Flags (Try swiping through launcher page) +* Enable All Apps Blur Flags (Try opening All Apps on supported devices) +* Enable MSDL Haptics Feedback Flags (Try gliding widget or icons across the homescreen) +* Make Predictive Back Gesture work on Android 13, 14, 15, 16 (Try swiping left or right on gesture-based navigational) +* Programmatically set Safe Mode status + +#### Known Bugs + +* App Icon may sometimes render with less than 0 in height/width causing blank icon to be rendered and crashing ISE on customising icons -> (31lLEflf) +* Any Lawnchair settings using IDP will crash the launcher -> Fixed in Lawnchair 16 pE Development 2 +* Icon pack isn't usable -> (DXo69Qzd) +* Dynamic icons will not be themed by launcher +* Full lists: https://trello.com/b/8IdvO81K/pe-lawnchair + +### Snapshot 6 + +This is a developer-focused change log: + +This snapshot marks the first time Lawnchair 16 is able to compile and build an APK! + +* Fix all issues with Java files in both `lawn` and `src` +* Make Lawnchair compilable (with instant crash) +* Move to KSP for Dagger code generation + +### Snapshot 5 + +This is a developer-focused change log: + +This snapshot now able to compile all sources (Kotlin files only) + +* Fix MORE MORE MORE `lawn` issues +* Use Gradle Version Catalog for consistent dependency version across all modules (Full implementation @ LawnchairLauncher/Lawnchair#5753) +* Magically fix ASM Instrumentation issues (I didn't do anything, it just works now) +* Fix ALL the issues in kotlin stage (`compileLawnWithQuickstepNightlyDebugKotlin`) +* Reintroduce some features from Lawnchair +* Add compatibility checks and workarounds for them +* Fix most issues with Java files in both `lawn` and `src` + +### Snapshot 4 + +This is a developer-focused change log: + +This snapshot marks the first time Lawnchair 16 is able to compile all Launcher3 sources! + +* Add `MSDLLib` to `platform_frameworks_libs_systemui` +* Add `contextualeducationlib` to `platform_frameworks_libs_systemui` +* Fix issues in both `lawn` and `src` modules +* Fix AIDL sources +* Resolve Lawnchair/LC-TODO lists +* Merge `wmshell.shared` res with res from `wmshell` +* Consistent build reproducibility by specifying dependencies in `build.gradle` +* Some ASM Instrumentation issues (and re-add some…) +* Update documentations + +### Snapshot 3 + +This is a developer-focused change log: + +Not a lot of errors left to go! + +* Finish correctly implementing all Dagger functions (?) +* Merge Lawnchair 15 Beta 1 into Bubble Tea + * Support for 16-kb page size devices +* Repository rebased and dropped commit + * Switch back from turbine-combined variant to javac variant for prebuilt SystemUI-core-16 because issues with LFS + * MORE MORE fixes regarding turbine-combined to javac +* Publish `platform_frameworks_libs_systemui` to pe 16-dev branch +* ATLEAST check to almost every launcher3 source file +* `Utils` module (stripped) +* Fix Dagger duplicated classes (because of Dagger dependency ksp/kapt mixing) +* Build reproducibility improvements by specifying dependencies in `build.gradle` files +* Fix some of the issues in both `lawn` and `src` modules + +### Snapshot 2 + +This is a developer-focused change log: + +This snapshot milestone marked the first time Lawnchair now able to compile all supplementary +modules, `src` + `lawn` will be in Snapshot 5 or Development 1 milestone. + +* Merge flags +* Fix some issues with launcher3 sources. +* A temporary workaround with framworks.jar not adding in anim module. +* Shared not having access to animationlib. +* **Switch from javac variant to turbine-combined variant for prebuilt SystemUI-core-16**. + +### From Initial snapshot 0 and 1 + +This is a developer-focused change log: +* Prebuilt updated to Android 16-0.0_r2 (Android 16.0.0 Release 2) +* Submodule have also been refreshed to A16r2 +* Baklava Compatlib (QuickSwitch compatibility not guaranteed) +* Refreshed internal documentation like prebuilt, systemUI diff --git a/VERIFICATION.md b/VERIFICATION.md new file mode 100644 index 0000000000..90f65d56f8 --- /dev/null +++ b/VERIFICATION.md @@ -0,0 +1,33 @@ +# Lawnchair verification + +Lawnchair apk are cryptographically signed and can be verified using two verifications system. +1. GitHub or SLSA attestations +2. SHA256 of android app certificate + +## SLSA Attestation + +Lawnchair repository is SLSA-Level 2 compliance and can be verified using a provenance. + +> [!NOTE] +> It is possible to verify without GitHub CLI by cross-referencing check from +> [GitHub Attestation][github-attestation] with [Sigstore Rekor][sigstore-rekor] + +1. Install GitHub CLI +2. Download the APK and attestation from [GitHub Attestation][github-attestation] +3. Run `gh attestation verify APK -R LawnchairLauncher/lawnchair`, replace {APK} with the + actual APK file +4. Done + +## Android App Certificate + +Lawnchair have two app certificates: +* Google Play: + `47:AC:92:63:1C:60:35:13:CC:8D:26:DD:9C:FF:E0:71:9A:8B:36:55:44:DC:CE:C2:09:58:24:EC:25:61:20:A7` +* Elsewhere: + `74:7C:36:45:B3:57:25:8B:2E:23:E8:51:E5:3C:96:74:7F:E0:AD:D0:07:E5:BA:2C:D9:7E:8C:85:57:2E:4D:C5` + +On Android, using a verification app like [AppVerifier][3p-appverifier] can ease up the verifying process. + +[github-attestation]: https://github.com/LawnchairLauncher/lawnchair/attestations +[sigstore-rekor]: https://search.sigstore.dev/ +[3p-appverifier]: https://github.com/soupslurpr/AppVerifier diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index f1f9966f78..7c8e5fdbd2 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -22,13 +22,6 @@ flag { bug: "316027081" } -flag { - name: "enable_grid_only_overview" - namespace: "launcher" - description: "Enable a grid-only overview without a focused task." - bug: "257950105" -} - flag { name: "enable_cursor_hover_states" namespace: "launcher" @@ -43,13 +36,6 @@ flag { bug: "302189128" } -flag { - name: "enable_overview_icon_menu" - namespace: "launcher" - description: "Enable updated overview icon and menu within task." - bug: "257950105" -} - flag { name: "enable_focus_outline" namespace: "launcher" @@ -85,13 +71,6 @@ flag { bug: "347281365" } -flag { - name: "enable_unfolded_two_pane_picker" - namespace: "launcher" - description: "Enables two pane widget picker for unfolded foldables" - bug: "313922374" -} - flag { name: "enable_tablet_two_pane_picker_v2" namespace: "launcher" @@ -113,13 +92,6 @@ flag { bug: "238475505" } -flag { - name: "enable_shortcut_dont_suggest_app" - namespace: "launcher" - description: "Enables don't suggest app shortcut for suggested apps" - bug: "319250810" -} - flag { name: "enable_support_for_archiving" namespace: "launcher" @@ -194,24 +166,6 @@ flag { } } -flag { - name: "use_activity_overlay" - namespace: "launcher" - description: "Use an activity for home screen overlay" - bug: "273828110" -} - -flag { - name: "enable_grid_migration_fix" - namespace: "launcher" - description: "Keep items in place when migrating to a bigger grid" - bug: "325286145" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} - flag { name: "enable_narrow_grid_restore" namespace: "launcher" @@ -237,54 +191,13 @@ flag { bug: "323886237" } -flag { - name: "enable_refactor_task_thumbnail" - namespace: "launcher" - description: "Enables rewritten version of TaskThumbnailViews in Overview" - bug: "331753115" -} - -flag { - name: "enable_handle_delayed_gesture_callbacks" - namespace: "launcher" - description: "Enables additional handling for delayed mid-gesture callbacks" - bug: "285636175" - metadata { - purpose: PURPOSE_BUGFIX - } -} - flag { name: "enable_fallback_overview_in_window" - namespace: "launcher" + namespace: "lse_desktop_experience" description: "Enables fallback recents opening inside of a window instead of an activity." bug: "292269949" } -flag { - name: "enable_smartspace_as_a_widget" - namespace: "launcher" - description: "Enables smartspace as a widget" - bug: "300140279" -} - -flag { - name: "enable_smartspace_removal_toggle" - namespace: "launcher" - description: "Enables smartspace removal toggle" - bug: "303471576" -} - -flag { - name: "enable_additional_home_animations" - namespace: "launcher" - description: "Enables custom home animations for non-running tasks" - bug: "237638627" - metadata { - purpose: PURPOSE_BUGFIX - } -} - flag { name: "enabled_folders_in_all_apps" namespace: "launcher" @@ -310,9 +223,422 @@ flag { } } +flag { + name: "enable_container_return_animations" + namespace: "launcher" + description: "Enables the container return animation mirroring launches." + bug: "341017746" +} + flag { name: "floating_search_bar" namespace: "launcher" description: "Search bar persists at the bottom of the screen across Launcher states" bug: "346408388" } + +flag { + name: "all_apps_sheet_for_handheld" + namespace: "launcher" + description: "All Apps will be presented on a bottom sheet in handheld mode" + bug: "374186088" +} + +flag { + name: "all_apps_blur" + namespace: "launcher" + description: "Content behind the all apps panel in Launcher will be blurred." + bug: "400827727" +} + +flag { + name: "enable_multi_instance_menu_taskbar" + namespace: "launcher" + description: "Menu in Taskbar with options to launch and manage multiple instances of the same app" + bug: "355237285" +} + +flag { + name: "use_new_icon_for_archived_apps" + namespace: "launcher" + description: "Archived apps will use new cloud icon in app title instead of overlay" + bug: "350758155" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "letter_fast_scroller" + namespace: "launcher" + description: "Change fast scroller to a lettered list" + bug: "358673724" +} + +flag { + name: "ignore_three_finger_trackpad_for_nav_handle_long_press" + namespace: "launcher" + description: "Ignore three finger trackpad event for nav handle long press" + bug: "342143522" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "work_scheduler_in_work_profile" + namespace: "launcher" + description: "Enables work scheduler view above the work pause button in work profile." + bug: "361589193" +} + +flag { + name: "one_grid_specs" + namespace: "launcher" + description: "Defines the new specs for grids based on OneGrid" + bug: "364711064" +} + +flag { + name: "one_grid_mounted_mode" + namespace: "launcher" + description: "Support a fixed landscape mode for handheld devices" + bug: "364711735" +} + +flag { + name: "one_grid_rotation_handling" + namespace: "launcher" + description: "New landscape approach for the workspace using different rows and columns in landscape and portrait" + bug: "364711814" +} + +flag { + name: "grid_migration_refactor" + namespace: "launcher" + description: "Refactor grid migration such that the code is simpler to understand and update" + bug: "358399271" +} + +flag { + name: "accessibility_scroll_on_allapps" + namespace: "launcher" + description: "Scroll to item position if accessibility focused" + bug: "265392261" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_dismiss_prediction_undo" + namespace: "launcher" + description: "Show an 'Undo' snackbar when users dismiss a predicted hotseat item" + bug: "270394476" +} + +flag { + name: "enable_all_apps_button_in_hotseat" + namespace: "launcher" + description: "Enables displaying the all apps button in the hotseat." + bug: "270393897" +} + +flag { + name: "taskbar_quiet_mode_change_support" + namespace: "launcher" + description: "Support changing quiet mode for user profiles in taskbar." + bug: "345760034" +} + +flag { + name: "enable_recents_window_proto_log" + namespace: "lse_desktop_experience" + description: "Enables tracking recents window logs in ProtoLog" + bug: "292269949" +} + +flag { + name: "enable_state_manager_proto_log" + namespace: "lse_desktop_experience" + description: "Enables tracking state manager logs in ProtoLog" + bug: "292269949" +} + +flag { + name: "enable_tiered_widgets_by_default_in_picker" + namespace: "launcher" + description: "Shows filtered set of widgets by default and an option to show all widgets in the widget picker" + bug: "356127021" +} + +flag { + name: "show_taskbar_pinning_popup_from_anywhere" + namespace: "launcher" + description: "Shows the pinning popup view after long-pressing or right-clicking anywhere on the pinned taskbar" + bug: "297325541" +} + +flag { + name: "enable_launcher_overview_in_window" + namespace: "lse_desktop_experience" + description: "Enables launcher recents opening inside of a window instead of being hosted in launcher activity." + bug: "292269949" +} + +flag { + name: "use_system_radius_for_app_widgets" + namespace: "launcher" + description: "Use system radius for enforced widget corners instead of a separate 16.dp value" + bug: "373351337" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_contrast_tiles" + namespace: "launcher" + description: "Enable launcher app contrast tiles." + bug: "341217082" +} + +flag { + name: "msdl_feedback" + namespace: "launcher" + description: "Enable MSDL feedback for Launcher interactions" + bug: "377496684" +} + +flag { + name: "enable_launcher_icon_shapes" + namespace: "launcher" + description: "Enable launcher icon shape customizations" + bug: "348708061" +} + +flag { + name: "predictive_back_to_home_polish" + namespace: "launcher" + description: "Enables workspace reveal animation for predictive back-to-home" + bug: "382453424" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "predictive_back_to_home_blur" + namespace: "launcher" + description: "Adds blur for predictive back-to-home" + bug: "342178850" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_launcher_visual_refresh" + namespace: "launcher" + description: "Adds refresh for font family, app longpress menu icons, and pagination dots" + bug: "395145453" +} + +flag { + name: "external_data_access" + namespace: "launcher" + description: "For LauncherProvider bug fixes and new static permissions as part of the cross oem backup / restore effort." + bug: "395145453" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "gsf_res" + namespace: "launcher" + description: "Adds refresh for font family. Needs to be fixed to be used in resources." + bug: "395145453" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "restore_archived_shortcuts" + namespace: "launcher" + description: "Makes sure pre-archived pinned shortcuts also get restored" + bug: "375414891" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "restore_archived_app_icons_from_db" + namespace: "launcher" + description: "Restores pre-archived icons from db when available, mimicing promise icons" + bug: "391913214" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_mouse_interaction_changes" + namespace: "launcher" + description: "Changes mouse interaction behavior" + bug: "388897603" +} + +flag { + name: "enable_alt_tab_kqs_on_connected_displays" + namespace: "lse_desktop_experience" + description: "Enable Alt + Tab KQS support on connected displays" + bug: "394007677" +} + +flag { + name: "expressive_theme_in_taskbar_and_navigation" + namespace: "launcher" + description: "Enables the expressive theme and GSF font styles for Taskbar and Gesture Navigation" + bug: "394613212" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_strict_mode" + namespace: "launcher" + description: "Enable Strict Mode for the Launcher app" + bug: "394651876" +} + +flag { + name: "enable_alt_tab_kqs_flatenning" + namespace: "lse_desktop_experience" + description: "Enable Alt + Tab KQS view to show apps in flattened structure" + bug: "382769617" +} + +flag { + name: "enable_gesture_nav_on_connected_displays" + namespace: "lse_desktop_experience" + description: "Enables gesture navigation handling on connected displays" + bug: "382130680" +} + +flag { + name: "enable_taskbar_behind_shade" + namespace: "lse_desktop_experience" + description: "Keeps taskbar behind notification shade when its pulled down" + bug: "343194358" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_scalability_for_desktop_experience" + namespace: "launcher" + description: "Enable more grid scale options on the launcher for desktop experience" + bug: "375491272" +} + +flag { + name: "enable_gesture_nav_horizontal_touch_slop" + namespace: "launcher" + description: "Enables horizontal touch slop checking in non-vertical fling navigation gestures" + bug: "394364217" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "sync_app_launch_with_taskbar_stash" + namespace: "launcher" + description: "Syncs the two animations (app launch, taskbar stash) so they play at the same time." + bug: "319162553" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "remove_apps_refresh_on_right_click" + namespace: "launcher" + description: "Remove predicted apps refresh on right click" + bug: "343650193" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_taskbar_for_direct_boot" + namespace: "launcher" + description: "Initializes parts of Taskbar before onUserUnlocked" + bug: "324485921" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_taskbar_ui_thread" + namespace: "launcher" + description: "Enable per-window thread for taskbar" + bug: "404636836" +} + +flag { + name: "enable_expressive_folder_expansion" + namespace: "launcher" + description: "Enables expressive folder expansion motion" + bug: "348708061" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "model_repository" + namespace: "launcher" + description: "Adds various data repositories for the model" + bug: "390572144" +} + +flag { + name: "home_screen_edit_improvements" + namespace: "launcher" + description: "Improves item removal and resizing within home screen" + bug: "416087474" +} + +flag { + name: "enable_widget_picker_refactor" + namespace: "launcher" + description: "Enables the refactored code for widget picker using separate activity and module" + bug: "370950552" +} + +flag { + name: "enable_reversible_home_action_corner" + namespace: "launcher" + description: "Enables home action corner to be reversible" + bug: "416664984" +} + +flag { + name: "enable_long_press_remove_shortcut" + namespace: "launcher" + description: "Enables remove app shortcut on long press menu" + bug: "419289205" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/aconfig/launcher_growth.aconfig b/aconfig/launcher_growth.aconfig new file mode 100644 index 0000000000..a880538dbf --- /dev/null +++ b/aconfig/launcher_growth.aconfig @@ -0,0 +1,9 @@ +package: "com.android.launcher3" +container: "system_ext" + +flag { + name: "enable_growth_nudge" + namespace: "desktop_oobe" + description: "Add growth nudge in launcher" + bug: "396165728" +} diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig new file mode 100644 index 0000000000..c6fb8bfc7f --- /dev/null +++ b/aconfig/launcher_overview.aconfig @@ -0,0 +1,106 @@ +package: "com.android.launcher3" +container: "system_ext" + +flag { + name: "enable_grid_only_overview" + namespace: "launcher_overview" + description: "Enable a grid-only overview without a focused task." + bug: "360204325" +} + +flag { + name: "enable_overview_icon_menu" + namespace: "launcher_overview" + description: "Enable updated overview icon and menu within task." + bug: "360205084" +} + +flag { + name: "enable_refactor_task_thumbnail" + namespace: "launcher_overview" + description: "Enables rewritten version of TaskThumbnailViews in Overview" + bug: "331754864" +} + +flag { + name: "enable_coroutine_threading_improvements" + namespace: "launcher_overview" + description: "Enables changes to the threading model of Launcher's coroutines implementation" + bug: "416206104" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_large_desktop_windowing_tile" + namespace: "launcher_overview" + description: "Makes the desktop tiles larger and moves them to the front of the list in Overview." + bug: "357860832" +} + +flag { + name: "enable_desktop_exploded_view" + namespace: "launcher_overview" + description: "Enables the non-overlapping layout for desktop windows in Overview mode." + bug: "378011776" +} + +flag { + name: "enable_expressive_dismiss_task_motion" + namespace: "launcher_overview" + description: "Enables expressive motion and animations for dismissing a task in Overview." + bug: "381239462" +} + +flag { + name: "enable_overview_on_connected_displays" + namespace: "launcher_overview" + description: "Enable overview on connected displays." + bug: "363251602" +} + +flag { + name: "enable_overview_background_wallpaper_blur" + namespace: "launcher_overview" + description: "Enable wallpaper blur in overview." + bug: "360297985" +} + +flag { + name: "enable_overview_desktop_tile_wallpaper_background" + namespace: "launcher_overview" + description: "Enable wallpaper background for desktop tasks in overview." + bug: "363257721" +} + +flag { + name: "enable_refactor_digital_wellbeing_toast" + namespace: "launcher_overview" + description: "Enables rewritten version of digital wellbeing toast in overview." + bug: "404838605" +} + +flag { + name: "enable_refactor_task_content_view" + namespace: "launcher_overview" + description: "Enables refactor wrapping TaskThumbnailView in a TaskContentView." + bug: "408971730" +} + +flag { + name: "enable_desktop_menu_on_secondary_display_bugfix" + namespace: "launcher_overview" + description: "Enable the 'Desktop' menu entry on full screen tiles on secondary display." + bug: "418822736" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_simultaneous_overview_trigger_on_extended_desktop" + namespace: "launcher_overview" + description: "Enable Overview to launch and exit simultaneously on default and external displays wherever it was triggered or quit from" + bug: "421128035" +} diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig index b98eee6e32..15ea9580d6 100644 --- a/aconfig/launcher_search.aconfig +++ b/aconfig/launcher_search.aconfig @@ -22,13 +22,6 @@ flag { bug: "308054233" } -flag { - name: "private_space_app_installer_button" - namespace: "launcher_search" - description: "This flag enables addition of App Installer button in Private Space container." - bug: "308064949" -} - flag { name: "private_space_restrict_accessibility_drag" namespace: "launcher_search" @@ -43,6 +36,15 @@ flag { bug: "289223923" } +flag { + name: "nudge_pill" + namespace: "launcher_search" + description: "This flag enables change the nav or 3 button to nudge related edu (pill or icon) for CtS." + bug: "409121556" + metadata { + purpose: PURPOSE_BUGFIX + } +} flag { name: "private_space_add_floating_mask_view" @@ -53,3 +55,9 @@ flag { purpose: PURPOSE_BUGFIX } } +flag { + name: "enable_qsb_on_hotseat" + namespace: "launcher_search" + description: "Enable Search App's widget on hotseat" + bug: "405226308" +} diff --git a/androidx-lib/build.gradle b/androidx-lib/build.gradle index ec0c42af04..39d8e88618 100644 --- a/androidx-lib/build.gradle +++ b/androidx-lib/build.gradle @@ -12,5 +12,5 @@ android { } } -addFrameworkJar('framework-15.jar') +addFrameworkJar('framework-16.jar') compileOnlyCommonJars() diff --git a/baseline-profile/build.gradle b/baseline-profile/build.gradle index 292b9057dd..eccb3db303 100644 --- a/baseline-profile/build.gradle +++ b/baseline-profile/build.gradle @@ -20,10 +20,16 @@ android { lawn { dimension = "app" } withQuickstep { dimension = "recents" } github { dimension = "channel" } + nightly { dimension = "channel" } play { dimension = "channel" } } testOptions.managedDevices.devices { + pixel7Api36(ManagedVirtualDevice) { + device = "Pixel 7" + apiLevel = 36 + systemImageSource = "google" + } pixel6Api33(ManagedVirtualDevice) { device = "Pixel 6" apiLevel = 33 @@ -35,7 +41,7 @@ android { // This is the configuration block for the Baseline Profile plugin. // You can specify to run the generators on a managed devices or connected devices. baselineProfile { - managedDevices += "pixel6Api33" + managedDevices += ["pixel6Api33", "pixel7Api36"] useConnectedDevices = false } diff --git a/baseline-profile/src/main/java/app/lawnchair/baseline/BaselineProfileGenerator.kt b/baseline-profile/src/main/java/app/lawnchair/baseline/BaselineProfileGenerator.kt index a55a86c1bd..c4fdc13af7 100644 --- a/baseline-profile/src/main/java/app/lawnchair/baseline/BaselineProfileGenerator.kt +++ b/baseline-profile/src/main/java/app/lawnchair/baseline/BaselineProfileGenerator.kt @@ -1,5 +1,7 @@ package app.lawnchair.baseline +import android.os.Build +import androidx.annotation.RequiresApi import androidx.benchmark.macro.junit4.BaselineProfileRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest @@ -33,9 +35,11 @@ import org.junit.runner.RunWith class BaselineProfileGenerator { @get:Rule + @RequiresApi(Build.VERSION_CODES.P) val rule = BaselineProfileRule() @Test + @RequiresApi(Build.VERSION_CODES.P) fun generate() { rule.collect(Constants.PACKAGE_NAME) { // This block defines the app's critical user journey. Here we are interested in diff --git a/build.gradle b/build.gradle index 875dc88122..4d91d9d375 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,14 @@ plugins { alias(libs.plugins.diffplug.spotless) } +// LC-Build-TODO: addFrameworkJar() doesn't work, what??? +def localFrameworkJar = new File("$rootDir/prebuilts/libs", "framework-16.jar") +tasks.withType(JavaCompile).configureEach { + classpath = files(localFrameworkJar) +} +tasks.withType(KotlinCompile).configureEach { + libraries.from(files(localFrameworkJar)) +} allprojects { plugins.withType(AndroidBasePlugin).configureEach { @@ -32,7 +40,7 @@ allprojects { } defaultConfig { minSdk 26 - targetSdk 35 + targetSdk 36 vectorDrawables.useSupportLibrary = true } lint { @@ -46,6 +54,7 @@ allprojects { } dependencies { implementation libs.androidx.core.ktx + implementation libs.androidx.core.animation } } @@ -53,7 +62,7 @@ allprojects { protobuf { // Configure the protoc executable protoc { - artifact = libs.protobuf.protoc.get().toString() + artifact = "com.google.protobuf:protoc:${libs.versions.protocVersion.get()}" } generateProtoTasks { all().configureEach { task -> @@ -119,9 +128,9 @@ allprojects { compileOnlyCommonJars = { dependencies { - compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-core.jar') - compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd.jar') - compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell-15.jar') + compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-core-16.jar') + compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd-16.jar') + compileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell-16.jar') compileOnly projects.compatLib compileOnly projects.compatLib.compatLibVQ @@ -130,6 +139,7 @@ allprojects { compileOnly projects.compatLib.compatLibVT compileOnly projects.compatLib.compatLibVU compileOnly projects.compatLib.compatLibVV + compileOnly projects.compatLib.compatLibVBaklava } } } @@ -144,20 +154,22 @@ final def ciRef = System.getenv("GITHUB_REF") ?: "" final def ciRunNumber = System.getenv("GITHUB_RUN_NUMBER") ?: "" final def isReleaseBuild = ciBuild && ciRef.contains("beta") final def devReleaseName = ciBuild ? "Dev.(#${ciRunNumber})" : "Dev.(${buildCommit})" -final def version = "15" -final def releaseName = "Beta 2" +final def version = "16" +final def releaseName = "Development 3 Release 0" final def versionDisplayName = "${version}.${isReleaseBuild ? releaseName : devReleaseName}" final def majorVersion = versionDisplayName.split("\\.")[0] final def quickstepMinSdk = "29" -final def quickstepMaxSdk = "35" +final def quickstepMaxSdk = "36" android { namespace "com.android.launcher3" defaultConfig { - // Lawnchair Launcher 15.0 Beta 2 - // See CONTRIBUTING.md#versioning-scheme - versionCode 15_00_02_02 + /* + * Lawnchair Launcher 16.0 Development 3 Release 1 + * see CONTRIBUTING.md#versioning-scheme + */ + versionCode 16_00_01_03_01 versionName "${versionDisplayName}" buildConfigField "String", "VERSION_DISPLAY_NAME", "\"${versionDisplayName}\"" buildConfigField "String", "MAJOR_VERSION", "\"${majorVersion}\"" @@ -245,12 +257,18 @@ android { debug { applicationIdSuffix ".debug" resValue("string", "derived_app_name", "Lawnchair (Debug)") + + manifestPlaceholders.quickstepMinSdk = "0" + manifestPlaceholders.quickstepMaxSdk = "100000" + buildConfigField "int", "QUICKSTEP_MIN_SDK", "0" + buildConfigField "int", "QUICKSTEP_MAX_SDK", "100000" } release { resValue("string", "derived_app_name", "Lawnchair") minifyEnabled true shrinkResources true + pseudoLocalesEnabled false proguardFiles proguardFilesFromAosp + "proguard.pro" } } @@ -343,7 +361,7 @@ android { withQuickstep { res.srcDirs = ['quickstep/res', 'quickstep/recents_ui_overrides/res'] - java.srcDirs = ['quickstep/src', 'quickstep/recents_ui_overrides/src'] + java.srcDirs = ['quickstep/src', 'quickstep/dagger', 'quickstep/recents_ui_overrides/src', 'quickstep/src_protolog'] manifest.srcFile "quickstep/AndroidManifest.xml" } } @@ -356,12 +374,14 @@ composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_build_reports") } -addFrameworkJar('framework-15.jar') +addFrameworkJar('framework-16.jar') dependencies { implementation projects.iconloaderlib implementation projects.searchuilib implementation projects.animationlib + implementation projects.msdllib + implementation projects.contextualeducationlib // Recents lib dependency withQuickstepCompileOnly projects.hiddenApi @@ -373,6 +393,7 @@ dependencies { withQuickstepCompileOnly projects.plugin withQuickstepImplementation projects.plugincore withQuickstepCompileOnly projects.common + withQuickstepCompileOnly projects.utils // QuickSwitch Compat withQuickstepImplementation projects.compatLib @@ -382,13 +403,15 @@ dependencies { withQuickstepImplementation projects.compatLib.compatLibVT withQuickstepImplementation projects.compatLib.compatLibVU withQuickstepImplementation projects.compatLib.compatLibVV + withQuickstepImplementation projects.compatLib.compatLibVBaklava withQuickstepImplementation projects.wmshell withQuickstepImplementation projects.flags implementation libs.androidx.dynamicanimation - implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd-15.jar') - implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell-15.jar') - withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'framework-15.jar') + implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'SystemUI-statsd-16.jar') + implementation fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'WindowManager-Shell-16.jar') + withQuickstepCompileOnly fileTree(dir: FRAMEWORK_PREBUILTS_DIR, include: 'framework-16.jar') + baselineProfile projects.baselineProfile coreLibraryDesugaring libs.android.desugarJdkLibs @@ -398,19 +421,27 @@ dependencies { implementation libs.androidx.recyclerview implementation libs.androidx.preference.ktx + implementation libs.javax.inject implementation libs.kotlinx.coroutines.android implementation libs.kotlinx.serialization.json implementation libs.chickenhook.restrictionbypass implementation libs.rikka.refine.runtime + implementation libs.androidx.activity.compose + implementation libs.androidx.constraintlayout + implementation libs.androidx.datastore.preferences + implementation platform(libs.compose.bom) implementation libs.compose.ui implementation libs.compose.ui.util + implementation libs.compose.ui.graphics + implementation libs.bundles.graphics debugImplementation libs.compose.ui.tooling implementation libs.compose.ui.tooling.preview implementation libs.compose.ui.google.fonts implementation libs.compose.foundation implementation libs.compose.material.icons + implementation libs.compose.material implementation libs.compose.runtime.livedata implementation libs.compose.material3 implementation libs.compose.material3.windowSizeClass @@ -453,10 +484,17 @@ dependencies { implementation libs.hoko.blur implementation libs.androidx.window + + ksp libs.dagger.compiler + implementation libs.dagger.hilt.android + ksp libs.dagger.hilt.compiler + + debugImplementation libs.leakcanary.android } ksp { arg("room.schemaLocation", "$projectDir/schemas") + arg("dagger.hilt.disableModulesHaveInstallInCheck", "true") } @@ -468,8 +506,8 @@ spotless { } kotlin { target("lawnchair/src/**/*.kt") - ktlint("1.8.0").customRuleSets([ - libs.composeRules.get().toString() + ktlint().customRuleSets([ + "io.nlopez.compose.rules:ktlint:0.4.27", ]).editorConfigOverride([ "ktlint_compose_compositionlocal-allowlist": "disabled", "ktlint_compose_lambda-param-event-trailing": "disabled", diff --git a/checks/Android.bp b/checks/Android.bp new file mode 100644 index 0000000000..dfd701efe0 --- /dev/null +++ b/checks/Android.bp @@ -0,0 +1,46 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library_host { + name: "Launcher3LintChecker", + srcs: ["src/**/*.kt"], + plugins: ["auto_service_plugin"], + libs: [ + "auto_service_annotations", + "lint_api", + ], + kotlincflags: ["-Xjvm-default=all"], +} + +java_test_host { + name: "Launcher3LintCheckerTest", + defaults: ["AndroidLintCheckerTestDefaults"], + srcs: ["tests/**/*.kt"], + data: [ + ":androidx.annotation_annotation", + ":dagger2", + ":kotlinx-coroutines-core", + ], + device_common_data: [ + ":framework", + ], + static_libs: [ + "Launcher3LintChecker", + ], +} diff --git a/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt new file mode 100644 index 0000000000..37358bbc0b --- /dev/null +++ b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UClass + +/** Detector to identify custom usage of Android's Dialog within the Launcher3 codebase. */ +class CustomDialogDetector : Detector(), SourceCodeScanner { + + override fun applicableSuperClasses(): List { + return listOf(DIALOG_CLASS_NAME) + } + + override fun visitClass(context: JavaContext, declaration: UClass) { + val superTypeClassNames = declaration.superTypes.mapNotNull { it.resolve()?.qualifiedName } + if (superTypeClassNames.contains(DIALOG_CLASS_NAME)) { + context.report( + ISSUE, + declaration, + context.getNameLocation(declaration), + "Class implements Dialog", + ) + } + } + + companion object { + private const val DIALOG_CLASS_NAME = "android.app.Dialog" + + @JvmField + val ISSUE = + Issue.create( + id = "IllegalUseOfCustomDialog", + briefDescription = "dialogs should not be used in Launcher", + explanation = + """ + Don't use custom Dialogs within the launcher code base, instead consider utilizing + AbstractFloatingView to display content that should float above the launcher where + it can be correctly managed for dismissal. + """ + .trimIndent(), + category = Category.CORRECTNESS, + priority = 10, + severity = Severity.ERROR, + implementation = + Implementation(CustomDialogDetector::class.java, Scope.JAVA_FILE_SCOPE), + ) + } +} diff --git a/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt new file mode 100644 index 0000000000..c77c42bf5d --- /dev/null +++ b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.launcher3.lint + +import CustomDialogDetector +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.android.tools.lint.detector.api.Issue +import com.google.auto.service.AutoService + +@AutoService(IssueRegistry::class) +@Suppress("UnstableApiUsage") +class Launcher3IssueRegistry : IssueRegistry() { + override val issues: List + get() = listOf(CustomDialogDetector.ISSUE) + + override val api: Int + get() = CURRENT_API + + override val minApi: Int + get() = 8 + + override val vendor: Vendor = + Vendor( + vendorName = "Android", + feedbackUrl = "http://b/issues/new?component=78010", + contact = "abegovic@google.com", + ) +} diff --git a/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt new file mode 100644 index 0000000000..2a37953038 --- /dev/null +++ b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.launcher3.lint + +import CustomDialogDetector +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +/** Test for [CustomDialogDetector]. */ +class CustomDialogDetectorTest : Launcher3LintDetectorTest() { + override fun getDetector(): Detector = CustomDialogDetector() + + override fun getIssues(): List = listOf(CustomDialogDetector.ISSUE) + + @Test + fun classDoesNotExtendDialog_noViolation() { + lint() + .files( + TestFiles.kotlin( + """ + package test.pkg + + class SomeClass + """ + .trimIndent() + ), + *androidStubs, + ) + .issues(CustomDialogDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun classDoesExtendDialog_violation() { + lint() + .files( + TestFiles.kotlin( + """ + package test.pkg + + import android.app.Dialog + + class SomeClass(context: Context) : Dialog(context) + """ + .trimIndent() + ), + *androidStubs, + ) + .issues(CustomDialogDetector.ISSUE) + .run() + .expect( + (""" + src/test/pkg/SomeClass.kt:5: Error: Class implements Dialog [IllegalUseOfCustomDialog] + class SomeClass(context: Context) : Dialog(context) + ~~~~~~~~~ + 1 errors, 0 warnings + """) + .trimIndent() + ) + } +} diff --git a/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt new file mode 100644 index 0000000000..09085c7c95 --- /dev/null +++ b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.launcher3.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestLintTask +import java.io.File +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Abstract class that should be used by any test for launcher 3 lint detectors. + * + * When you write your test, ensure that you pass [androidStubs] as part of your [TestFiles] + * definition. + */ +@RunWith(JUnit4::class) +abstract class Launcher3LintDetectorTest : LintDetectorTest() { + + /** + * Customize the lint task to disable SDK usage completely. This ensures that running the tests + * in Android Studio has the same result as running the tests in atest + */ + override fun lint(): TestLintTask = + super.lint().allowMissingSdk(true).sdkHome(File("/dev/null")) + + companion object { + private val libraryNames = + arrayOf( + "androidx.annotation_annotation.jar", + "dagger2.jar", + "framework.jar", + "kotlinx-coroutines-core.jar", + ) + + /** + * This file contains stubs of framework APIs and System UI classes for testing purposes + * only. The stubs are not used in the lint detectors themselves. + */ + val androidStubs = + libraryNames + .map { TestFiles.LibraryReferenceTestFile(File(it).canonicalFile) } + .toTypedArray() + } +} diff --git a/compatLib/README.md b/compatLib/README.md index 2698ccba01..82a31bfe9a 100644 --- a/compatLib/README.md +++ b/compatLib/README.md @@ -1,15 +1,24 @@ # Lawnchair Quickstep Compat Library -The `compatLib` library helps integrate Lawnchair with QuickSwitch while ensuring backward-compatibility with older Android versions. +The `compatLib` library helps integrate Lawnchair with Recents +(also known as QuickSwitch, Quickstep, or sometimes, Lawnstep) +while ensuring backward-compatibility with older Android versions. Each subdirectory of the `compatLib`, denoted by a letter (e.g., `compatLibVQ` for Android 10), refers to the compatibility code for that specific Android version. -| Library | Android version | -|-------------|-----------------| -| compatLibVQ | 10 | -| compatLibVR | 11 | -| compatLibVS | 12 | -| compatLibVT | 13 | -| compatLibVU | 14 | -| compatLibVV | 15 | +Starting with Android 16 and above, the `compatLib` will denoted by the codename of the Android version +(e.g., `compatLibVBaklava` for Android 16). + +| Library | Android version | +|-------------------|-----------------| +| compatLibVQ | 10 | +| compatLibVR | 11 | +| compatLibVS | 12 | +| compatLibVT | 13 | +| compatLibVU | 14 | +| compatLibVV | 15 | +| compatLibVBaklava | 16 | + +Keep in mind that this list does not guarantee Recents compatibility with your Android versions, +as the implementation may still be in progress or not fully functional. diff --git a/compatLib/build.gradle b/compatLib/build.gradle index 7813d645c4..5c3b5a0e13 100644 --- a/compatLib/build.gradle +++ b/compatLib/build.gradle @@ -3,6 +3,7 @@ plugins { } android { + buildToolsVersion "36.1.0" namespace "app.lawnchair.compatlib" buildFeatures { diff --git a/compatLib/compatLibVBaklava/build.gradle b/compatLib/compatLibVBaklava/build.gradle new file mode 100644 index 0000000000..21fb60ec36 --- /dev/null +++ b/compatLib/compatLibVBaklava/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'com.android.library' +} + +android { + namespace 'app.lawnchair.compatlib.sixteen' +} + +addFrameworkJar('framework-16.jar') + +dependencies { + api projects.compatLib.compatLibVV +} diff --git a/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/ActivityManagerCompatVBaklava.java b/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/ActivityManagerCompatVBaklava.java new file mode 100644 index 0000000000..736200c667 --- /dev/null +++ b/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/ActivityManagerCompatVBaklava.java @@ -0,0 +1,7 @@ +package app.lawnchair.compatlib.sixteen; + +import androidx.annotation.RequiresApi; +import app.lawnchair.compatlib.fifteen.ActivityManagerCompatVV; + +@RequiresApi(36) +public class ActivityManagerCompatVBaklava extends ActivityManagerCompatVV {} diff --git a/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/ActivityOptionsCompatVBaklava.java b/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/ActivityOptionsCompatVBaklava.java new file mode 100644 index 0000000000..83da4a7ed6 --- /dev/null +++ b/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/ActivityOptionsCompatVBaklava.java @@ -0,0 +1,45 @@ +package app.lawnchair.compatlib.sixteen; + +import android.app.ActivityOptions; +import android.content.Context; +import android.os.Handler; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import app.lawnchair.compatlib.fifteen.ActivityOptionsCompatVV; + +@RequiresApi(36) +public class ActivityOptionsCompatVBaklava extends ActivityOptionsCompatVV { + @NonNull + @Override + public ActivityOptions makeCustomAnimation( + @NonNull Context context, + int enterResId, + int exitResId, + @NonNull final Handler callbackHandler, + @Nullable final Runnable startedListener, + @Nullable final Runnable finishedListener) { + return ActivityOptions.makeCustomAnimation( + context, + enterResId, + exitResId, + 0, + callbackHandler, + new ActivityOptions.OnAnimationStartedListener() { + @Override + public void onAnimationStarted(long elapsedRealTime) { + if (startedListener != null) { + startedListener.run(); + } + } + }, + new ActivityOptions.OnAnimationFinishedListener() { + @Override + public void onAnimationFinished(long elapsedRealTime) { + if (finishedListener != null) { + finishedListener.run(); + } + } + }); + } +} diff --git a/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/QuickstepCompatFactoryVBaklava.java b/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/QuickstepCompatFactoryVBaklava.java new file mode 100644 index 0000000000..223828f1fe --- /dev/null +++ b/compatLib/compatLibVBaklava/src/main/java/app/lawnchair/compatlib/sixteen/QuickstepCompatFactoryVBaklava.java @@ -0,0 +1,31 @@ +package app.lawnchair.compatlib.sixteen; + +import android.window.RemoteTransition; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import app.lawnchair.compatlib.ActivityManagerCompat; +import app.lawnchair.compatlib.ActivityOptionsCompat; +import app.lawnchair.compatlib.RemoteTransitionCompat; +import app.lawnchair.compatlib.fifteen.QuickstepCompatFactoryVV; + +@RequiresApi(36) +public class QuickstepCompatFactoryVBaklava extends QuickstepCompatFactoryVV { + + @NonNull + @Override + public ActivityManagerCompat getActivityManagerCompat() { + return new ActivityManagerCompatVBaklava(); + } + + @NonNull + @Override + public ActivityOptionsCompat getActivityOptionsCompat() { + return new ActivityOptionsCompatVBaklava(); + } + + @NonNull + @Override + public RemoteTransitionCompat getRemoteTransitionCompat() { + return RemoteTransition::new; + } +} diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/compose/facade/core/BaseComposeFacade.kt similarity index 71% rename from quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt rename to compose/facade/core/BaseComposeFacade.kt index a8b5112860..bc7ba4700e 100644 --- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt +++ b/compose/facade/core/BaseComposeFacade.kt @@ -14,11 +14,13 @@ * limitations under the License. */ -package com.android.quickstep.task.viewmodel +package com.android.launcher3.compose.core -import kotlinx.coroutines.flow.MutableStateFlow +import android.content.Context +import android.view.View -class TaskViewData { - // This is typically a View concern but it is used to invalidate rendering in other Views - val scale = MutableStateFlow(1f) +interface BaseComposeFacade { + fun isComposeAvailable(): Boolean + + fun initComposeView(appContext: Context): View } diff --git a/compose/facade/core/widgetpicker/NoOpWidgetPickerModule.kt b/compose/facade/core/widgetpicker/NoOpWidgetPickerModule.kt new file mode 100644 index 0000000000..3cba82d54e --- /dev/null +++ b/compose/facade/core/widgetpicker/NoOpWidgetPickerModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.compose.core.widgetpicker + +import dagger.Binds +import dagger.Module + +/** + * A module that provides a no-op [WidgetPickerComposeWrapper] for dagger graph that doesn't + * involve widget picker e.g. launcher preview OR when compose is disabled via build flag. + */ +@Module +interface NoOpWidgetPickerModule { + @Binds + fun bindWidgetPickerWrapper(noOp: NoOpWidgetPickerComposeWrapper): WidgetPickerComposeWrapper +} \ No newline at end of file diff --git a/compose/facade/core/widgetpicker/WidgetPickerComposeWrapper.kt b/compose/facade/core/widgetpicker/WidgetPickerComposeWrapper.kt new file mode 100644 index 0000000000..93bf32a83e --- /dev/null +++ b/compose/facade/core/widgetpicker/WidgetPickerComposeWrapper.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.compose.core.widgetpicker + +import com.android.launcher3.widgetpicker.WidgetPickerActivity +import com.android.launcher3.widgetpicker.WidgetPickerConfig +import javax.annotation.Nonnull +import javax.inject.Inject + +/** + * A wrapper for widget picker activity that is responsible for displaying the compose based + * widget picker in [WidgetPickerActivity] when compose is enabled via build flag. + */ +interface WidgetPickerComposeWrapper { + fun showAllWidgets( + activity: WidgetPickerActivity, + @Nonnull + widgetPickerConfig: WidgetPickerConfig + ) +} + +/** + * A No-op [WidgetPickerComposeWrapper] that doesn't include widget picker in dagger graph that + * don't involve widget picker e.g. launcher preview OR when compose is disabled via build flag. + */ +class NoOpWidgetPickerComposeWrapper @Inject constructor() : WidgetPickerComposeWrapper { + override fun showAllWidgets( + activity: WidgetPickerActivity, + @Nonnull + widgetPickerConfig: WidgetPickerConfig + ) { + error("Widget picker with compose is not supported") + } +} diff --git a/compose/facade/disabled/ComposeFacade.kt b/compose/facade/disabled/ComposeFacade.kt new file mode 100644 index 0000000000..c1cbfff0c3 --- /dev/null +++ b/compose/facade/disabled/ComposeFacade.kt @@ -0,0 +1,32 @@ +/* + * 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.launcher3.compose + +import android.content.Context +import android.view.View +import com.android.launcher3.compose.core.BaseComposeFacade + +object ComposeFacade : BaseComposeFacade { + override fun isComposeAvailable(): Boolean = false + + override fun initComposeView(appContext: Context): View { + error( + "Compose is not available. Make sure to check isComposeAvailable() before calling any" + + " other function on ComposeFacade." + ) + } +} diff --git a/compose/facade/disabled/widgetpicker/LauncherWidgetPickerModule.kt b/compose/facade/disabled/widgetpicker/LauncherWidgetPickerModule.kt new file mode 100644 index 0000000000..65635817e0 --- /dev/null +++ b/compose/facade/disabled/widgetpicker/LauncherWidgetPickerModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.compose.widgetpicker + +import com.android.launcher3.compose.core.widgetpicker.NoOpWidgetPickerModule +import dagger.Module + +/** + * A no-op module that is used when compose is disabled in launcher. + */ +@Module(includes = [NoOpWidgetPickerModule::class]) +class LauncherWidgetPickerModule diff --git a/compose/facade/enabled/ComposeFacade.kt b/compose/facade/enabled/ComposeFacade.kt new file mode 100644 index 0000000000..d98a979f29 --- /dev/null +++ b/compose/facade/enabled/ComposeFacade.kt @@ -0,0 +1,28 @@ +/* + * 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.launcher3.compose + +import android.content.Context +import android.view.View +import androidx.compose.ui.platform.ComposeView +import com.android.launcher3.compose.core.BaseComposeFacade + +object ComposeFacade : BaseComposeFacade { + override fun isComposeAvailable(): Boolean = true + + override fun initComposeView(appContext: Context): View = ComposeView(appContext) +} diff --git a/compose/facade/enabled/widgetpicker/LauncherWidgetPickerModule.kt b/compose/facade/enabled/widgetpicker/LauncherWidgetPickerModule.kt new file mode 100644 index 0000000000..7694077760 --- /dev/null +++ b/compose/facade/enabled/widgetpicker/LauncherWidgetPickerModule.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.compose.widgetpicker + +import com.android.launcher3.compose.core.widgetpicker.WidgetPickerComposeWrapper +import com.android.launcher3.widgetpicker.WidgetPickerComponent +import com.android.launcher3.widgetpicker.WidgetPickerComposeWrapperImpl +import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository +import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository +import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository +import com.android.launcher3.widgetpicker.datasource.ConfigResourceFeaturedWidgetsDataSource +import com.android.launcher3.widgetpicker.datasource.FeaturedWidgetsDataSource +import com.android.launcher3.widgetpicker.datasource.InMemoryWidgetSearchAlgorithm +import com.android.launcher3.widgetpicker.datasource.WidgetsSearchAlgorithm +import com.android.launcher3.widgetpicker.repository.WidgetsRepositoryImpl +import com.android.launcher3.widgetpicker.repository.WidgetAppIconsRepositoryImpl +import com.android.launcher3.widgetpicker.repository.WidgetUsersRepositoryImpl +import dagger.Binds +import dagger.Module + +/** + * A module that installs widget picker for launcher. + */ +@Module(subcomponents = [WidgetPickerComponent::class]) +interface LauncherWidgetPickerModule { + @Binds + fun bindWidgetPickerComposeWrapper( + impl: WidgetPickerComposeWrapperImpl + ): WidgetPickerComposeWrapper + + @Binds + fun bindWidgetUsersRepository(impl: WidgetUsersRepositoryImpl): WidgetUsersRepository + + @Binds + fun bindWidgetsRepository(impl: WidgetsRepositoryImpl): WidgetsRepository + + @Binds + fun bindWidgetAppIconsRepository(impl: WidgetAppIconsRepositoryImpl): WidgetAppIconsRepository + + @Binds + fun bindFeaturedWidgetsDataSource( + impl: ConfigResourceFeaturedWidgetsDataSource + ): FeaturedWidgetsDataSource + + @Binds + fun bindWidgetsSearchAlgorithm( + impl: InMemoryWidgetSearchAlgorithm + ): WidgetsSearchAlgorithm +} diff --git a/compose/features/com/android/launcher3/util/ComposeUtils.kt b/compose/features/com/android/launcher3/util/ComposeUtils.kt new file mode 100644 index 0000000000..ad7319a411 --- /dev/null +++ b/compose/features/com/android/launcher3/util/ComposeUtils.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util + +import android.animation.TimeInterpolator +import android.graphics.drawable.Drawable +import androidx.compose.animation.core.Easing +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.graphics.drawable.toBitmap + +/** + * Converts a [TimeInterpolator] to a compose [Easing]. + * + * This function allows you to use Android's [TimeInterpolator] instances, such as those defined in + * `android.R.interpolator`, directly with Compose animations that expect an [Easing]. + */ +fun TimeInterpolator.toComposeEasing(): Easing = Easing { fraction -> getInterpolation(fraction) } + +/** + * A composable function that takes a [Drawable] and converts it into a [Painter] which can be used + * to draw the drawable on a Compose canvas. + * + * This function uses [remember] to cache the conversion of the [Drawable] to an [ImageBitmap]. This + * means that if the same [Drawable] instance is provided across recompositions, the conversion will + * only happen once, improving performance. + */ +@Composable +fun painterResource(drawable: Drawable): Painter { + val imageBitmap = remember(drawable) { drawable.toBitmap().asImageBitmap() } + return BitmapPainter(imageBitmap) +} + + +/** + * A multi-preview annotation that displays the same Composable with different font scales. + */ +@Preview(name = "normal font", group = "font scales", fontScale = 1f) +@Preview(name = "large font", group = "font scales", fontScale = 2f) +annotation class FontScalePreviews diff --git a/compose/features/com/android/launcher3/widgetpicker/WidgetPickerComposeWrapperImpl.kt b/compose/features/com/android/launcher3/widgetpicker/WidgetPickerComposeWrapperImpl.kt new file mode 100644 index 0000000000..c408620089 --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/WidgetPickerComposeWrapperImpl.kt @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalView +import com.android.launcher3.Launcher +import com.android.launcher3.R +import com.android.launcher3.compose.ComposeFacade +import com.android.launcher3.compose.core.widgetpicker.WidgetPickerComposeWrapper +import com.android.launcher3.concurrent.annotations.BackgroundContext +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.util.ApiWrapper +import com.android.launcher3.widgetpicker.WidgetPickerConfig.Companion.EXTRA_IS_PENDING_WIDGET_DRAG +import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository +import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository +import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository +import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener +import com.android.launcher3.widgetpicker.listeners.WidgetPickerDragItemListener +import com.android.launcher3.widgetpicker.shared.model.HostConstraint +import com.android.launcher3.widgetpicker.shared.model.WidgetHostInfo +import com.android.launcher3.widgetpicker.shared.model.isAppWidget +import com.android.launcher3.widgetpicker.theme.darkWidgetPickerColors +import com.android.launcher3.widgetpicker.theme.lightWidgetPickerColors +import com.android.launcher3.widgetpicker.ui.WidgetInteractionInfo +import com.android.launcher3.widgetpicker.ui.WidgetPickerEventListeners +import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerTheme +import javax.inject.Inject +import javax.inject.Provider +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.launch + +/** + * An helper that bootstraps widget picker UI (from [WidgetPickerComponent]) in to + * [WidgetPickerActivity] when compose is available and widget picker refactor flags are on. + * + * Sets up the bindings necessary for widget picker component. + */ +class WidgetPickerComposeWrapperImpl +@Inject +constructor( + private val widgetPickerComponentProvider: Provider, + private val widgetsRepository: WidgetsRepository, + private val widgetUsersRepository: WidgetUsersRepository, + private val widgetAppIconsRepository: WidgetAppIconsRepository, + @BackgroundContext private val backgroundContext: CoroutineContext, + @ApplicationContext private val appContext: Context, + private val apiWrapper: ApiWrapper, +) : WidgetPickerComposeWrapper { + + override fun showAllWidgets( + activity: WidgetPickerActivity, + widgetPickerConfig: WidgetPickerConfig, + ) { + val widgetPickerComponent = newWidgetPickerComponent(widgetPickerConfig) + val callbacks = activity.buildEventListeners(widgetPickerConfig, apiWrapper) + + val fullWidgetsCatalog = widgetPickerComponent.getFullWidgetsCatalog() + val composeView = ComposeFacade.initComposeView(activity.asContext()) as ComposeView + + composeView.apply { + setContent { + val scope = rememberCoroutineScope() + val view = LocalView.current + + val widgetPickerColors = + if (isSystemInDarkTheme()) { + darkWidgetPickerColors() + } else { + lightWidgetPickerColors() + } + + MaterialTheme { // TODO(b/408283627): Use launcher theme. + WidgetPickerTheme(colors = widgetPickerColors) { + val eventListeners = remember { callbacks } + fullWidgetsCatalog.Content(eventListeners) + } + } + + DisposableEffect(view) { + scope.launch { initializeRepositories() } + + onDispose { cleanUpRepositories() } + } + } + } + + checkNotNull(activity.dragLayer).addView(composeView) + } + + private fun newWidgetPickerComponent( + widgetPickerConfig: WidgetPickerConfig + ): WidgetPickerComponent { + return widgetPickerComponentProvider + .get() + .build( + widgetsRepository = widgetsRepository, + widgetUsersRepository = widgetUsersRepository, + widgetAppIconsRepository = widgetAppIconsRepository, + widgetHostInfo = + WidgetHostInfo( + title = + widgetPickerConfig.title + ?: appContext.resources.getString(R.string.widget_button_text), + description = widgetPickerConfig.description, + constraints = widgetPickerConfig.asHostConstraints(), + showDragShadow = !widgetPickerConfig.isForHomeScreen, + ), + backgroundContext = backgroundContext, + ) + } + + private fun initializeRepositories() { + widgetsRepository.initialize() + widgetUsersRepository.initialize() + widgetAppIconsRepository.initialize() + } + + private fun cleanUpRepositories() { + widgetsRepository.cleanUp() + widgetUsersRepository.cleanUp() + widgetAppIconsRepository.cleanUp() + } + + companion object { + private const val HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING = + "WidgetPickerActivity.OnWidgetInteraction" + + private fun WidgetPickerActivity.buildEventListeners( + widgetPickerConfig: WidgetPickerConfig, + apiWrapper: ApiWrapper, + ) = + object : WidgetPickerEventListeners { + override fun onClose() { + finish() + } + + override fun onWidgetInteraction(widgetInteractionInfo: WidgetInteractionInfo) { + if (widgetPickerConfig.isForHomeScreen) { + handleWidgetInteractionForHomeScreen(widgetInteractionInfo, apiWrapper) + } else { + handleWidgetInteractionForExternalHost(widgetInteractionInfo) + } + } + } + + /** + * Handles communication with the home screen about the "add" and "drag" interactions on + * widgets within widget picker. + * + * For home screen, we register a listener that is called back when home screen is shown; + * - WidgetPickerDragItemListener: bootstraps the drag helper that displays the shadow and + * handles the drag until completion. + * - WidgetPickerAddItemListener: once launcher is shown, triggers the flow to add the + * widget to workspace. + */ + private fun WidgetPickerActivity.handleWidgetInteractionForHomeScreen( + interactionInfo: WidgetInteractionInfo, + apiWrapper: ApiWrapper, + ) { + val interactionListener = + when (interactionInfo) { + is WidgetInteractionInfo.WidgetDragInfo -> + WidgetPickerDragItemListener( + mimeType = interactionInfo.mimeType, + widgetInfo = interactionInfo.widgetInfo, + widgetPreview = interactionInfo.previewInfo, + previewRect = interactionInfo.bounds, + previewWidth = interactionInfo.widthPx, + ) + + is WidgetInteractionInfo.WidgetAddInfo -> + WidgetPickerAddItemListener(interactionInfo.widgetInfo) + } + Launcher.ACTIVITY_TRACKER.registerCallback( + interactionListener, + HOME_SCREEN_WIDGET_INTERACTION_REASON_STRING, + ) + startActivity( + /*intent=*/ Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setPackage(packageName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /*options=*/ apiWrapper.createFadeOutAnimOptions().toBundle(), + ) + finish() + } + + /** + * Handles communication with the external host about the "add" and "drag" interactions on + * widgets within widget picker. + * - In case of drag and drop, finishes the activity with result indicating that there is a + * pending drag [EXTRA_IS_PENDING_WIDGET_DRAG] (that would contain the widget info as part + * of clip data) that the host should be handling. + * - In case of add, finishes the activity with result containing extra information about + * the widget being added (namely [Intent.EXTRA_COMPONENT_NAME] and [Intent.EXTRA_USER]. + */ + private fun WidgetPickerActivity.handleWidgetInteractionForExternalHost( + widgetInteractionInfo: WidgetInteractionInfo + ) { + when (widgetInteractionInfo) { + is WidgetInteractionInfo.WidgetDragInfo -> + setResult(RESULT_OK, Intent().putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true)) + + is WidgetInteractionInfo.WidgetAddInfo -> { + val widgetInfo = widgetInteractionInfo.widgetInfo + if (widgetInfo.isAppWidget()) { + val providerInfo = widgetInfo.appWidgetProviderInfo + setResult( + RESULT_OK, + Intent().apply { + putExtra(Intent.EXTRA_COMPONENT_NAME, providerInfo.provider) + putExtra(Intent.EXTRA_USER, providerInfo.profile) + }, + ) + } else { + throw IllegalStateException( + "AppWidgetInfo not provided for external host drag" + ) + } + } + } + + finish() + } + + /** Builds the host constraints to provide to the widget picker module. */ + fun WidgetPickerConfig.asHostConstraints() = buildList { + if (filteredUsers.isNotEmpty()) { + add(HostConstraint.HostUserConstraint(filteredUsers)) + } + if (!isForHomeScreen) { + add(HostConstraint.NoShortcutsConstraint) + } + if (categoryInclusionFilter != 0 || categoryExclusionFilter != 0) { + add( + HostConstraint.HostCategoryConstraint( + categoryInclusionMask = categoryInclusionFilter, + categoryExclusionMask = categoryExclusionFilter, + ) + ) + } + } + } +} diff --git a/compose/features/com/android/launcher3/widgetpicker/datasource/ConfigResourceFeaturedWidgetsDataSource.kt b/compose/features/com/android/launcher3/widgetpicker/datasource/ConfigResourceFeaturedWidgetsDataSource.kt new file mode 100644 index 0000000000..5c11c7fe84 --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/datasource/ConfigResourceFeaturedWidgetsDataSource.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.datasource + +import android.content.Context +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.R +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize +import com.android.launcher3.widgetpicker.shared.model.PickableWidget +import com.android.launcher3.widgetpicker.shared.model.WidgetApp +import com.android.launcher3.widgetpicker.shared.model.isAppWidget +import java.util.Arrays +import java.util.stream.Collectors +import javax.inject.Inject + +/** + * An implementation of [FeaturedWidgetsDataSource] that provides featured widgets based on a static + * configuration from resources and pre-defined size templates. + * + * Only appwidgets; no shortcuts + */ +@LauncherAppSingleton +class ConfigResourceFeaturedWidgetsDataSource +@Inject +constructor( + @ApplicationContext private val appContext: Context, + private val idp: InvariantDeviceProfile, +) : FeaturedWidgetsDataSource { + // the package part in component name e.g. "com.example" in {com.example/widget.Provider} + private var eligiblePackages: Set = emptySet() + + override suspend fun initialize() { + if (eligiblePackages.isEmpty()) { + eligiblePackages = + Arrays.stream( + appContext.resources.getStringArray(R.array.default_featured_widget_apps) + ) + .collect(Collectors.toSet()) + } + } + + override suspend fun getFeaturedWidgets(widgetApps: List): List { + val widgetsByContainerSize = + widgetApps + .shuffled() + // pick only one of user profiles + .distinctBy { Pair(it.id.packageName, it.id.category) } + .flatMap { it.widgets } + .filter { + eligiblePackages.isEmpty() || + eligiblePackages.contains(it.id.componentName.packageName) + } + .groupBy { + WidgetPreviewContainerSize( + it.sizeInfo.containerSpanX, + it.sizeInfo.containerSpanY, + ) + } + + val selected: MutableList = mutableListOf() + val usedAppIds: MutableSet = mutableSetOf() + + val sizesToPick = + WidgetPreviewContainerSize.pickTemplateForFeaturedWidgets( + idp.getDeviceProfile(appContext) + ) + for (sizeToPick in sizesToPick) { + widgetsByContainerSize[sizeToPick]?.shuffled()?.let { items -> + for (item in items) { + if ( + item.widgetInfo.isAppWidget() && + !usedAppIds.contains(item.appId.packageName) + ) { + selected.add(item) + usedAppIds.add(item.appId.packageName) + break + } + } + } + } + + return selected + } + + override fun cleanup() { + eligiblePackages = emptySet() + } +} diff --git a/compose/features/com/android/launcher3/widgetpicker/datasource/FeaturedWidgetsDataSource.kt b/compose/features/com/android/launcher3/widgetpicker/datasource/FeaturedWidgetsDataSource.kt new file mode 100644 index 0000000000..2438d9fcc7 --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/datasource/FeaturedWidgetsDataSource.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.datasource + +import com.android.launcher3.widgetpicker.shared.model.PickableWidget +import com.android.launcher3.widgetpicker.shared.model.WidgetApp + +/** + * An interface representing a datasource that provides featured widgets. + */ +interface FeaturedWidgetsDataSource { + /** + * Perform any initializations here. + */ + suspend fun initialize() + + /** + * Provides widgets from the given list that can be featured in widget picker. + */ + suspend fun getFeaturedWidgets(widgetApps: List): List + + /** + * Clear any service bindings or state here. + */ + fun cleanup() +} diff --git a/compose/features/com/android/launcher3/widgetpicker/datasource/InMemoryWidgetsSearchAlgorithm.kt b/compose/features/com/android/launcher3/widgetpicker/datasource/InMemoryWidgetsSearchAlgorithm.kt new file mode 100644 index 0000000000..16e8ca48cd --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/datasource/InMemoryWidgetsSearchAlgorithm.kt @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.datasource + +import com.android.launcher3.concurrent.annotations.BackgroundContext +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.widgetpicker.shared.model.WidgetApp +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +/** + * A simple in-memory implementation of [WidgetsSearchAlgorithm] that matches input query against + * the app labels, widget title and description. + * + * More weight is given to app names than widget title; and description has lowest weight in scoring + * the results. + */ +@LauncherAppSingleton +class InMemoryWidgetSearchAlgorithm @Inject constructor( + @BackgroundContext + private val backgroundContext: CoroutineContext, +) : WidgetsSearchAlgorithm { + override suspend fun initialize() {} + + override suspend fun searchWidgets( + query: String, + corpus: List + ): List = coroutineScope { + // Ideally, uses search only one word, but supporting multiple words. + val queryWords = query.trim().lowercase().split("\\s+".toRegex()) + .filter { it.isNotBlank() } + + if (queryWords.isEmpty()) { + return@coroutineScope emptyList() + } + + val results = corpus.map { widgetApp -> + async(backgroundContext) { + matchAndScoreWidgetApp(widgetApp, queryWords) + } + } + + results.awaitAll() + .filter { it.first.widgets.isNotEmpty() && it.second > 0 } + .sortedByDescending { it.second } + .map { it.first } // Return just the WidgetApp + } + + private fun matchAndScoreWidgetApp( + widgetApp: WidgetApp, + queryWords: List + ): Pair { + val appTitleScore = widgetApp.title?.let { + matchAndCalculateScore( + queryWords = queryWords, + targetText = widgetApp.title.toString(), + matchType = MatchType.APP_TITLE + ) + } ?: 0 + + val scoredWidgets = widgetApp.widgets.map { widget -> + val labelScore = + matchAndCalculateScore( + queryWords = queryWords, + targetText = widget.label, + matchType = MatchType.WIDGET_LABEL + ) + val descriptionScore = widget.description?.let { + matchAndCalculateScore( + queryWords = queryWords, + targetText = it.toString(), + matchType = MatchType.WIDGET_DESCRIPTION + ) + } ?: 0 + + val totalWidgetScore = labelScore + descriptionScore + widget to totalWidgetScore + } + .filter { it.second > 0 } + .sortedByDescending { it.second } + + val totalAppScore = appTitleScore + scoredWidgets.sumOf { it.second } + + return if (appTitleScore > 0) { + widgetApp to appTitleScore + totalAppScore + } else { + widgetApp.copy( + widgets = scoredWidgets.map { it.first } + ) to totalAppScore + } + } + + private fun matchAndCalculateScore( + queryWords: List, + targetText: String, + matchType: MatchType + ): Int { + var totalScore = 0 + val wordsInTarget = targetText + .lowercase() + .split("\\s+".toRegex()).filter { it.isNotBlank() } + + for (queryWord in queryWords) { + var wordScore = 0 + + for (targetWord in wordsInTarget) { + wordScore = wordScore.coerceAtLeast( + calculateSingleWordScore( + queryWord, + targetWord, + matchType + ) + ) + } + totalScore += wordScore + } + return totalScore + } + + private fun calculateSingleWordScore( + queryWord: String, + targetWord: String, + matchType: MatchType + ): Int { + val baseScore = when { + // exact matches are score higher. + targetWord == queryWord -> 2 + // Then the matches that begin with given input as prefix + targetWord.startsWith(queryWord) -> 1 + else -> 0 + } + + return baseScore * matchType.weightFactor + } + + private enum class MatchType(val weightFactor: Int) { + // Highest weight to app title matches; users are likely to search for apps + APP_TITLE(12), + + // Medium weight to widget labels (lower than app title as some widgets might have app title + // in name and based on number of items, might add unnecessary weight). + WIDGET_LABEL(3), + + // Lowest weight to description; description might might not be as impactful as the labels + // or app title; but, it might still help in cases where alternate words for the widgets + // functionality are used (e.g. stocks vs watchlist). + WIDGET_DESCRIPTION(1) + } + + override fun cleanup() {} +} diff --git a/compose/features/com/android/launcher3/widgetpicker/datasource/WidgetsSearchAlgorithm.kt b/compose/features/com/android/launcher3/widgetpicker/datasource/WidgetsSearchAlgorithm.kt new file mode 100644 index 0000000000..e4a98bb362 --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/datasource/WidgetsSearchAlgorithm.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.datasource + +import com.android.launcher3.widgetpicker.shared.model.WidgetApp + +/** + * An interface representing algorithm used to search widgets for widget picker. + */ +interface WidgetsSearchAlgorithm { + /** + * Perform any initializations here + */ + suspend fun initialize() + + /** + * Returns apps and its widgets that match the given input string. + */ + suspend fun searchWidgets(query: String, corpus: List): List + + /** + * Clear any service bindings or state here. + */ + fun cleanup() +} diff --git a/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerAddItemListener.kt b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerAddItemListener.kt new file mode 100644 index 0000000000..988d016bbd --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerAddItemListener.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.listeners + +import android.view.View +import com.android.launcher3.Launcher +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY +import com.android.launcher3.PendingAddItemInfo +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO +import com.android.launcher3.util.ContextTracker.SchedulerCallback +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.android.launcher3.widget.PendingAddShortcutInfo +import com.android.launcher3.widget.PendingAddWidgetInfo +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo + +/** + * A callback listener (for tap-to-add flow) that handles adding a widget from a separate widget + * picker activity. Invoked once widget picker is closed and home screen is showing / ready. + * + * Also logs to stats logger once widget is added. + */ +class WidgetPickerAddItemListener(private val widgetInfo: WidgetInfo) : + SchedulerCallback { + override fun init(launcher: Launcher?, isHomeStarted: Boolean): Boolean { + checkNotNull(launcher) + + val pendingAddItemInfo: PendingAddItemInfo = + when (widgetInfo) { + is WidgetInfo.AppWidgetInfo -> { + val launcherProviderInfo = + LauncherAppWidgetProviderInfo.fromProviderInfo( + launcher, + widgetInfo.appWidgetProviderInfo, + ) + PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) + } + + is WidgetInfo.ShortcutInfo -> + PendingAddShortcutInfo( + ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo) + ) + } + + val view = View(launcher) + view.tag = pendingAddItemInfo + + launcher.accessibilityDelegate?.addToWorkspace( + /*item=*/ pendingAddItemInfo, + /*accessibility=*/ false, + ) { + launcher.statsLogManager + .logger() + .withItemInfo(pendingAddItemInfo) + .log(LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP) + } + return false // don't receive any more callbacks as we got launcher and handled it + } +} diff --git a/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerDragItemListener.kt b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerDragItemListener.kt new file mode 100644 index 0000000000..9d13b9bb49 --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/listeners/WidgetPickerDragItemListener.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.listeners + +import android.graphics.Rect +import android.view.View +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY +import com.android.launcher3.PendingAddItemInfo +import com.android.launcher3.dragndrop.BaseItemDragListener +import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader.WidgetPreviewInfo +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.android.launcher3.widget.PendingAddShortcutInfo +import com.android.launcher3.widget.PendingAddWidgetInfo +import com.android.launcher3.widget.PendingItemDragHelper +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo +import com.android.launcher3.widgetpicker.shared.model.WidgetPreview +import com.android.launcher3.widgetpicker.shared.model.isAppWidget + +/** + * A callback listener of type [BaseItemDragListener] that handles widget drag and drop from widget + * picker hosted in a separate activity than home screen. + * + * Responsible for initializing the [PendingItemDragHelper] that then handles the rest of the drag + * and drop (including showing a drag shadow for the widget). + * + * @param mimeType a mime type used by widget picker when attaching this listener for a specific + * widget's drag and drop session. + * @param widgetInfo metadata of the widget being dragged + * @param widgetPreview provides the preview information for widgets + * @param previewRect the bounds of widget's preview offset by the point of long press + * @param previewWidth width of the preview as it appears in the widget picker. + */ +class WidgetPickerDragItemListener( + private val mimeType: String, + private val widgetInfo: WidgetInfo, + private val widgetPreview: WidgetPreview, + previewRect: Rect, + previewWidth: Int, +) : BaseItemDragListener(previewRect, previewWidth, previewWidth) { + override fun getMimeType(): String = mimeType + + override fun createDragHelper(): PendingItemDragHelper { + val pendingAddItemInfo: PendingAddItemInfo = + when (widgetInfo) { + is WidgetInfo.AppWidgetInfo -> { + val launcherProviderInfo = + LauncherAppWidgetProviderInfo.fromProviderInfo( + mLauncher, + widgetInfo.appWidgetProviderInfo, + ) + PendingAddWidgetInfo(launcherProviderInfo, CONTAINER_WIDGETS_TRAY) + } + + is WidgetInfo.ShortcutInfo -> + PendingAddShortcutInfo( + ShortcutConfigActivityInfoVO(widgetInfo.launcherActivityInfo) + ) + } + + val view = View(mLauncher) + view.tag = pendingAddItemInfo + + val dragHelper = PendingItemDragHelper(view) + + if (widgetInfo.isAppWidget()) { + setAppWidgetPreviewInfo(widgetPreview, widgetInfo, dragHelper) + } // shortcut preview is fetched by home screen. + + return dragHelper + } + + private fun setAppWidgetPreviewInfo( + appWidgetPreview: WidgetPreview, + widgetInfo: WidgetInfo.AppWidgetInfo, + dragHelper: PendingItemDragHelper, + ) { + val info = WidgetPreviewInfo() + when (appWidgetPreview) { + is WidgetPreview.BitmapWidgetPreview -> { + info.previewBitmap = appWidgetPreview.bitmap + info.providerInfo = widgetInfo.appWidgetProviderInfo + } + + is WidgetPreview.ProviderInfoWidgetPreview -> { + info.providerInfo = appWidgetPreview.providerInfo + } + + is WidgetPreview.RemoteViewsWidgetPreview -> { + info.remoteViews = appWidgetPreview.remoteViews + info.providerInfo = widgetInfo.appWidgetProviderInfo + } + + else -> + throw IllegalStateException( + "Unsupported preview type when dropping widget to launcher" + ) + } + dragHelper.setWidgetPreviewInfo(info) + } +} diff --git a/compose/features/com/android/launcher3/widgetpicker/repository/WidgetAppIconsRepositoryImpl.kt b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetAppIconsRepositoryImpl.kt new file mode 100644 index 0000000000..e1802b4e28 --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetAppIconsRepositoryImpl.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.repository + +import com.android.launcher3.icons.IconCache +import com.android.launcher3.icons.cache.CacheLookupFlag +import com.android.launcher3.model.data.PackageItemInfo +import com.android.launcher3.widgetpicker.data.repository.WidgetAppIconsRepository +import com.android.launcher3.widgetpicker.shared.model.AppIcon +import com.android.launcher3.widgetpicker.shared.model.AppIconBadge +import com.android.launcher3.widgetpicker.shared.model.WidgetAppIcon +import com.android.launcher3.widgetpicker.shared.model.WidgetAppId +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import javax.inject.Inject + +/** + * An implementation of [WidgetAppIconsRepository] that provides the app icons for the widget picker + * using the [IconCache]. + */ +class WidgetAppIconsRepositoryImpl @Inject constructor( + private val iconCache: IconCache +) : WidgetAppIconsRepository { + override fun initialize() {} // nothing to do here. + + override fun getAppIcon(widgetAppId: WidgetAppId) = callbackFlow { + trySend(WidgetAppIcon(AppIcon.PlaceHolderAppIcon, AppIconBadge.NoBadge)) + + val category = widgetAppId.category + val packageItemInfo = if (category != null) { + PackageItemInfo(widgetAppId.packageName, category, widgetAppId.userHandle) + } else { + PackageItemInfo(widgetAppId.packageName, widgetAppId.userHandle) + } + + iconCache.updateIconInBackground({ itemInfoWithIcon -> + itemInfoWithIcon?.let { + if (itemInfoWithIcon.bitmap.isLowRes) { + trySend( + WidgetAppIcon( + icon = AppIcon.LowResColorIcon(itemInfoWithIcon.bitmap.color), + badge = AppIconBadge.NoBadge + ) + ) + } else { + trySend( + WidgetAppIcon( + icon = AppIcon.HighResBitmapIcon(itemInfoWithIcon.bitmap.icon), + badge = itemInfoWithIcon.bitmap.getBadgeDrawableInfo()?.let { + AppIconBadge.DrawableBadge( + it.drawableRes, + it.colorRes + ) + } ?: AppIconBadge.NoBadge + ) + ) + } + } + }, packageItemInfo, CacheLookupFlag.DEFAULT_LOOKUP_FLAG) + + awaitClose() + } + + override fun cleanUp() {} // nothing to do here. +} diff --git a/compose/features/com/android/launcher3/widgetpicker/repository/WidgetUsersRepositoryImpl.kt b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetUsersRepositoryImpl.kt new file mode 100644 index 0000000000..e8c7c82d5a --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetUsersRepositoryImpl.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.repository + +import android.content.Context +import android.os.UserHandle +import android.os.UserManager +import com.android.launcher3.concurrent.annotations.BackgroundContext +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.model.StringCache +import com.android.launcher3.pm.UserCache +import com.android.launcher3.util.SafeCloseable +import com.android.launcher3.widgetpicker.data.repository.WidgetUsersRepository +import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfile +import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfileType +import com.android.launcher3.widgetpicker.shared.model.WidgetUserProfiles +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +/** + * An implementation of [WidgetUsersRepository] that provides user profile info to widget picker + * by looking up the [UserCache]. + */ +class WidgetUsersRepositoryImpl @Inject constructor( + @ApplicationContext private val appContext: Context, + private val userCache: UserCache, + @BackgroundContext + private val backgroundContext: CoroutineContext +) : WidgetUsersRepository { + private val userManagerService = appContext.getSystemService(UserManager::class.java) + private var stringCache: StringCache = StringCache() + private var closableUseChangeListener: SafeCloseable? = null + private val _userProfiles = MutableStateFlow(null) + private var workProfileUser: UserHandle? = null + private var backgroundScope = CoroutineScope( + SupervisorJob() + + backgroundContext + + CoroutineName("widgetUsersRepositoryBackgroundWork") + ) + + override fun initialize() { + backgroundScope.launch { + stringCache.loadStrings(appContext) + maybeUpdate(changedUser = null) + + closableUseChangeListener?.close() + closableUseChangeListener = userCache.addUserEventListener { userHandle, _ -> + maybeUpdate(userHandle) + } + } + } + + override fun observeUserProfiles(): Flow = _userProfiles.asStateFlow() + + override fun getWorkProfileUser(): UserHandle? = workProfileUser + + override fun cleanUp() { + closableUseChangeListener?.close() + backgroundScope.apply { + if (isActive) { + cancel() + } + } + } + + private fun maybeUpdate(changedUser: UserHandle?) { + check(userManagerService != null) + + workProfileUser = + userCache.userProfiles.firstOrNull { userCache.getUserInfo(it).isWork } + val needsUpdate = changedUser == null || changedUser == workProfileUser + + if (needsUpdate) { + val isUserQuiet = + workProfileUser?.let { + userManagerService.isQuietModeEnabled( + workProfileUser + ) + } ?: false + + _userProfiles.update { + WidgetUserProfiles( + personal = WidgetUserProfile( + type = WidgetUserProfileType.PERSONAL, + label = stringCache.widgetsPersonalTab, + paused = false, + pausedProfileMessage = null, + ), + work = workProfileUser?.let { + WidgetUserProfile( + type = WidgetUserProfileType.WORK, + label = stringCache.widgetsWorkTab, + paused = isUserQuiet, + pausedProfileMessage = stringCache.workProfilePausedTitle, + ) + } + ) + } + } + } +} diff --git a/compose/features/com/android/launcher3/widgetpicker/repository/WidgetsRepositoryImpl.kt b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetsRepositoryImpl.kt new file mode 100644 index 0000000000..23ab58588a --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/repository/WidgetsRepositoryImpl.kt @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.repository + +import android.content.Context +import com.android.launcher3.DeviceProfile +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.concurrent.annotations.BackgroundContext +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.model.WidgetItem +import com.android.launcher3.model.WidgetsModel +import com.android.launcher3.model.data.PackageItemInfo +import com.android.launcher3.pm.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader +import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize +import com.android.launcher3.widget.util.WidgetSizes +import com.android.launcher3.widgetpicker.data.repository.WidgetsRepository +import com.android.launcher3.widgetpicker.datasource.FeaturedWidgetsDataSource +import com.android.launcher3.widgetpicker.datasource.WidgetsSearchAlgorithm +import com.android.launcher3.widgetpicker.shared.model.PickableWidget +import com.android.launcher3.widgetpicker.shared.model.WidgetApp +import com.android.launcher3.widgetpicker.shared.model.WidgetAppId +import com.android.launcher3.widgetpicker.shared.model.WidgetId +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo +import com.android.launcher3.widgetpicker.shared.model.WidgetPreview +import com.android.launcher3.widgetpicker.shared.model.WidgetSizeInfo +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * An implementation of the [WidgetsRepository] that provides widgets for widget picker using the + * [WidgetsModel], [FeaturedWidgetsDataSource] & enables search using the provided + * [WidgetsSearchAlgorithm]. + */ +class WidgetsRepositoryImpl +@Inject +constructor( + @ApplicationContext private val appContext: Context, + idp: InvariantDeviceProfile, + private val widgetsModel: WidgetsModel, + private val featuredWidgetsDataSource: FeaturedWidgetsDataSource, + private val searchAlgorithm: WidgetsSearchAlgorithm, + @BackgroundContext private val backgroundContext: CoroutineContext, +) : WidgetsRepository { + private val deviceProfile = idp.getDeviceProfile(appContext) + private val backgroundScope = + CoroutineScope(SupervisorJob() + backgroundContext + CoroutineName("WidgetsRepository")) + + private val _widgetItemsByPackage = MutableStateFlow>(emptyList()) + private val databaseWidgetPreviewLoader = DatabaseWidgetPreviewLoader(appContext, deviceProfile) + + override fun initialize() { + // TODO(b/419495339): Remove the model executor requirement from widgets model and replace + // with scope.launch + MODEL_EXECUTOR.execute { + widgetsModel.update(/* packageUser= */ null) + _widgetItemsByPackage.update { + widgetsModel.widgetsByPackageItemForPicker.toPickableWidgets(deviceProfile) + } + } + + backgroundScope.launch { featuredWidgetsDataSource.initialize() } + } + + override fun observeWidgets(): Flow> = _widgetItemsByPackage.asStateFlow() + + override suspend fun getWidgetPreview(id: WidgetId): WidgetPreview { + val componentKey = ComponentKey(id.componentName, id.userHandle) + val widgetItem = + widgetsModel.widgetsByComponentKey[componentKey] + ?: return WidgetPreview.PlaceholderWidgetPreview + + val previewSizePx = + WidgetSizes.getWidgetSizePx(deviceProfile, widgetItem.spanX, widgetItem.spanY) + val preview = + withContext(backgroundContext) { + val result = + databaseWidgetPreviewLoader.generatePreviewInfoBg( + widgetItem, + previewSizePx.width, + previewSizePx.height, + ) + when { + result.remoteViews != null -> + WidgetPreview.RemoteViewsWidgetPreview(result.remoteViews) + + result.providerInfo != null -> + WidgetPreview.ProviderInfoWidgetPreview(result.providerInfo) + + result.previewBitmap != null -> + WidgetPreview.BitmapWidgetPreview(result.previewBitmap) + + else -> WidgetPreview.PlaceholderWidgetPreview + } + } + + return preview + } + + override suspend fun searchWidgets(query: String): List = + searchAlgorithm.searchWidgets(query, _widgetItemsByPackage.value) + + override fun cleanUp() { + _widgetItemsByPackage.update { emptyList() } + backgroundScope.apply { + if (isActive) { + cancel() + } + } + } + + override fun getFeaturedWidgets(): Flow> { + return _widgetItemsByPackage + .map { widgets -> featuredWidgetsDataSource.getFeaturedWidgets(widgets) } + .flowOn(backgroundContext) + } + + companion object { + private fun Map>.toPickableWidgets( + deviceProfile: DeviceProfile + ) = map { (packageItemInfo, widgetItems) -> + val widgetAppId = + WidgetAppId( + packageName = packageItemInfo.packageName, + userHandle = packageItemInfo.user, + category = packageItemInfo.widgetCategory, + ) + + WidgetApp( + id = widgetAppId, + title = packageItemInfo.title, + widgets = + widgetItems.map { widgetItem -> + val previewSize = + WidgetSizes.getWidgetSizePx( + deviceProfile, + widgetItem.spanX, + widgetItem.spanY, + ) + val containerSpan = + WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile) + val containerSize = + WidgetSizes.getWidgetSizePx( + deviceProfile, + containerSpan.spanX, + containerSpan.spanY, + ) + + PickableWidget( + id = + WidgetId( + componentName = widgetItem.componentName, + userHandle = widgetItem.user, + ), + appId = widgetAppId, + label = widgetItem.label, + description = widgetItem.description, + widgetInfo = + if (widgetItem.widgetInfo != null) { + WidgetInfo.AppWidgetInfo( + appWidgetProviderInfo = widgetItem.widgetInfo.clone() + ) + } else { + check(widgetItem.activityInfo is ShortcutConfigActivityInfoVO) + WidgetInfo.ShortcutInfo( + launcherActivityInfo = widgetItem.activityInfo.mInfo + ) + }, + sizeInfo = + WidgetSizeInfo( + spanX = widgetItem.spanX, + spanY = widgetItem.spanY, + widthPx = previewSize.width, + heightPx = previewSize.height, + containerSpanX = containerSpan.spanX, + containerSpanY = containerSpan.spanY, + containerWidthPx = containerSize.width, + containerHeightPx = containerSize.height, + ), + ) + }, + ) + } + } +} diff --git a/compose/features/com/android/launcher3/widgetpicker/theme/Colors.kt b/compose/features/com/android/launcher3/widgetpicker/theme/Colors.kt new file mode 100644 index 0000000000..bb7ccb3116 --- /dev/null +++ b/compose/features/com/android/launcher3/widgetpicker/theme/Colors.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widgetpicker.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import com.android.launcher3.R +import com.android.launcher3.widgetpicker.ui.theme.WidgetPickerColors + +/** + * An adapter that maps the resource tokens for widget picker dark colors to the + * [WidgetPickerColors] accepted by the widget picker module. + */ +@Composable +fun darkWidgetPickerColors() = + WidgetPickerColors( + // Bottom Sheet + sheetBackground = colorResource(R.color.widget_picker_primary_surface_color_dark), + dragHandle = colorResource(R.color.widget_picker_collapse_handle_color_dark), + sheetTitle = colorResource(R.color.widget_picker_title_color_dark), + sheetDescription = colorResource(R.color.widget_picker_description_color_dark), + + // Expand collapse list + expandCollapseIndicatorIcon = + colorResource(R.color.widget_picker_expand_icon_button_color_dark), + expandCollapseIndicatorBackground = + colorResource(R.color.widget_picker_expand_icon_button_background_dark), + expandableListItemsBackground = + colorResource(R.color.widget_picker_expandable_list_items_background_dark), + + // List header + selectedListHeaderBackground = + colorResource(R.color.widget_picker_clickable_list_header_background_dark), + unselectedListHeaderBackground = Color.Transparent, + featuredHeaderLeadingIconBackground = + colorResource(R.color.widget_picker_featured_header_icon_background_dark), + featuredHeaderLeadingIcon = + colorResource(R.color.widget_picker_featured_header_icon_color_dark), + listHeaderTitle = colorResource(R.color.widget_picker_header_app_title_color_dark), + listHeaderSubTitle = colorResource(R.color.widget_picker_header_app_subtitle_color_dark), + + // Error message + noWidgetsErrorText = colorResource(R.color.widget_picker_no_widget_error_color_dark), + + // Widgets container + widgetsContainerBackground = + colorResource(R.color.widget_picker_widgets_container_background_dark), + + // App icon + placeholderAppIcon = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f), + + // Widget details + widgetLabel = colorResource(R.color.widget_cell_title_color_dark), + widgetSpanText = colorResource(R.color.widget_cell_subtitle_color_dark), + widgetDescription = colorResource(R.color.widget_cell_subtitle_color_dark), + addButtonContent = colorResource(R.color.widget_picker_add_button_text_color_dark), + addButtonBackground = colorResource(R.color.widget_picker_add_button_background_color_dark), + + // Widget preview + widgetPlaceholderBackground = + colorResource(R.color.widget_picker_preview_placeholder_background_dark), + widgetPlaceholderContent = + colorResource(R.color.widget_picker_preview_placeholder_content_dark), + + // Floating Toolbar + toolbarBackground = colorResource(R.color.widget_picker_toolbar_background_dark), + toolbarTabSelectedBackground = + colorResource(R.color.widget_picker_toolbar_selected_tab_background_dark), + toolbarTabUnSelectedBackground = + colorResource(R.color.widget_picker_toolbar_unselected_tab_background_dark), + toolbarTabContent = colorResource(R.color.widget_picker_toolbar_tab_content_color_dark), + + // Search bar + searchBarBackground = colorResource(R.color.widget_picker_search_bar_background_color_dark), + searchBarPlaceholderText = colorResource(R.color.widget_picker_search_text_color_dark), + searchBarText = colorResource(R.color.widget_picker_search_text_color_dark), + searchBarSearchIcon = colorResource(R.color.widget_picker_search_text_color_dark), + searchBarClearButtonIcon = colorResource(R.color.widget_picker_search_text_color_dark), + searchBarBackButtonIcon = colorResource(R.color.widget_picker_search_text_color_dark), + searchBarCursor = colorResource(R.color.widget_picker_search_cursor_color_dark), + ) + +/** + * An adapter that maps the resource tokens for widget picker light colors to the + * [WidgetPickerColors] accepted by the widget picker module + */ +@Composable +fun lightWidgetPickerColors() = + WidgetPickerColors( + // Bottom Sheet + sheetBackground = colorResource(R.color.widget_picker_primary_surface_color_light), + dragHandle = colorResource(R.color.widget_picker_collapse_handle_color_light), + sheetTitle = colorResource(R.color.widget_picker_title_color_light), + sheetDescription = colorResource(R.color.widget_picker_description_color_light), + + // Expand collapse list + expandCollapseIndicatorIcon = + colorResource(R.color.widget_picker_expand_icon_button_color_light), + expandCollapseIndicatorBackground = + colorResource(R.color.widget_picker_expand_icon_button_background_light), + expandableListItemsBackground = + colorResource(R.color.widget_picker_expandable_list_items_background_light), + + // List header + selectedListHeaderBackground = + colorResource(R.color.widget_picker_clickable_list_header_background_light), + unselectedListHeaderBackground = Color.Transparent, + featuredHeaderLeadingIconBackground = + colorResource(R.color.widget_picker_featured_header_icon_background_light), + featuredHeaderLeadingIcon = + colorResource(R.color.widget_picker_featured_header_icon_color_light), + listHeaderTitle = colorResource(R.color.widget_picker_header_app_title_color_light), + listHeaderSubTitle = colorResource(R.color.widget_picker_header_app_subtitle_color_light), + + // Error message + noWidgetsErrorText = colorResource(R.color.widget_picker_no_widget_error_color_light), + + // Widgets container + widgetsContainerBackground = + colorResource(R.color.widget_picker_widgets_container_background_light), + + // App icon + placeholderAppIcon = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f), + + // Widget details + widgetLabel = colorResource(R.color.widget_cell_title_color_light), + widgetSpanText = colorResource(R.color.widget_cell_subtitle_color_light), + widgetDescription = colorResource(R.color.widget_cell_subtitle_color_light), + addButtonContent = colorResource(R.color.widget_picker_add_button_text_color_light), + addButtonBackground = + colorResource(R.color.widget_picker_add_button_background_color_light), + + // Widget preview + widgetPlaceholderBackground = + colorResource(R.color.widget_picker_preview_placeholder_background_light), + widgetPlaceholderContent = + colorResource(R.color.widget_picker_preview_placeholder_content_light), + + // Floating Toolbar + toolbarBackground = colorResource(R.color.widget_picker_toolbar_background_light), + toolbarTabSelectedBackground = + colorResource(R.color.widget_picker_toolbar_selected_tab_background_light), + toolbarTabUnSelectedBackground = + colorResource(R.color.widget_picker_toolbar_unselected_tab_background_light), + toolbarTabContent = colorResource(R.color.widget_picker_toolbar_tab_content_color_light), + + // Search bar + searchBarBackground = + colorResource(R.color.widget_picker_search_bar_background_color_light), + searchBarPlaceholderText = colorResource(R.color.widget_picker_search_text_color_light), + searchBarText = colorResource(R.color.widget_picker_search_text_color_light), + searchBarSearchIcon = colorResource(R.color.widget_picker_search_text_color_light), + searchBarClearButtonIcon = colorResource(R.color.widget_picker_search_text_color_light), + searchBarBackButtonIcon = colorResource(R.color.widget_picker_search_text_color_light), + searchBarCursor = colorResource(R.color.widget_picker_search_cursor_color_light), + ) diff --git a/compose/tests/com/android/launcher3/helper/TestHelper.kt b/compose/tests/com/android/launcher3/helper/TestHelper.kt new file mode 100644 index 0000000000..001f963def --- /dev/null +++ b/compose/tests/com/android/launcher3/helper/TestHelper.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.helper + +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.NativeKeyEvent +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +val NATIVE_DPAD_DOWN = NativeKeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_DPAD_DOWN) +val NATIVE_TAB = NativeKeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_TAB) +val NavigateDownKeyEvent = KeyEvent(NATIVE_DPAD_DOWN) +val NavigateDownTabEvent = KeyEvent(NATIVE_TAB) + +fun hasMinTouchArea(minWidth: Dp = 48.dp, minHeight: Dp = 48.dp): SemanticsMatcher = + SemanticsMatcher("touch area is smaller than $minWidth x $minHeight") { node -> + val density: Density = node.root?.density ?: error("root node not available!") + with(density) { + val rect = node.touchBoundsInRoot + val size = node.size + (rect.width.toDp() >= minWidth && rect.height.toDp() >= minHeight) || + (size.width.toDp() >= minWidth && size.height.toDp() >= minHeight) + } + } diff --git a/compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt b/compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt new file mode 100644 index 0000000000..288604f9eb --- /dev/null +++ b/compose/tests/com/android/launcher3/widget/AddWidgetConfigTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.widget + +import android.appwidget.AppWidgetManager +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.Launcher +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.testcomponent.WidgetConfigActivity +import com.android.launcher3.util.BaseLauncherActivityTest +import com.android.launcher3.util.BlockingBroadcastReceiver +import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator +import com.android.launcher3.util.Wait +import com.android.launcher3.util.rule.ShellCommandRule +import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape +import com.android.launcher3.util.ui.TestViewHelpers +import com.android.launcher3.util.workspace.FavoriteItemsTransaction +import com.android.launcher3.widgetpicker.listeners.WidgetPickerAddItemListener +import com.android.launcher3.widgetpicker.shared.model.WidgetInfo +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test verifies that when adding a widget to homescreen using [WidgetPickerAddItemListener], if the + * widget has a configuration activity, it is shown correctly. + */ +@LargeTest +@RunWith(AndroidJUnit4::class) +class AddWidgetConfigTest : BaseLauncherActivityTest() { + @get:Rule val grantWidgetRule: ShellCommandRule = ShellCommandRule.grantWidgetBind() + + private lateinit var widgetInfo: LauncherAppWidgetProviderInfo + private lateinit var appWidgetManager: AppWidgetManager + + private var widgetId = 0 + + @Before + @Throws(Exception::class) + fun setUp() { + widgetInfo = TestViewHelpers.findWidgetProvider(/* hasConfigureScreen= */ true) + appWidgetManager = AppWidgetManager.getInstance(targetContext()) + } + + @Test + @PortraitLandscape + @Throws(Throwable::class) + fun testWidgetConfig() { + runTest(acceptConfig = true) + } + + @Test + @PortraitLandscape + @Throws(Throwable::class) + fun testConfigCancelled() { + runTest(acceptConfig = false) + } + + /** @param acceptConfig accept the config activity */ + @Throws(Throwable::class) + private fun runTest(acceptConfig: Boolean) { + FavoriteItemsTransaction(targetContext()).commit() + loadLauncherSync() + + // Add widget to home screen + val monitor = WidgetConfigStartupMonitor() + launcherActivity.executeOnLauncher { l: Launcher -> + val addItemListener = WidgetPickerAddItemListener(WidgetInfo.AppWidgetInfo(widgetInfo)) + addItemListener.init(l, /* isHomeStarted= */ true) + } + + uiDevice.waitForIdle() + + // Widget id for which the config activity was opened + widgetId = monitor.widgetId + + // Verify that the widget id is valid and bound + assertThat(appWidgetManager.getAppWidgetInfo(widgetId)).isNotNull() + setResult(acceptConfig) + + if (acceptConfig) { + launcherActivity.getOnceNotNull("Widget was not added") { l: Launcher -> + // Close the resize frame before searching for widget + AbstractFloatingView.closeAllOpenViews(l) + l.workspace.mapOverItems(WidgetSearchCondition()) + } + assertThat(appWidgetManager.getAppWidgetInfo(widgetId)).isNotNull() + } else { + // Verify that the widget id is deleted. + Wait.atMost( + "no widget with id", + { appWidgetManager.getAppWidgetInfo(widgetId) == null }, + ) + } + } + + private fun setResult(success: Boolean) { + InstrumentationRegistry.getInstrumentation() + .targetContext + .sendBroadcast( + WidgetConfigActivity.getCommandIntent( + WidgetConfigActivity::class.java, + if (success) "clickOK" else "clickCancel", + ) + ) + uiDevice.waitForIdle() + } + + /** Condition for searching widget id */ + private inner class WidgetSearchCondition : ItemOperator { + override fun evaluate(info: ItemInfo?, view: View): Boolean { + return info is LauncherAppWidgetInfo && + info.providerName == widgetInfo.provider && + info.appWidgetId == widgetId + } + } + + /** Broadcast receiver for receiving widget config activity status. */ + private class WidgetConfigStartupMonitor : + BlockingBroadcastReceiver(WidgetConfigActivity::class.java.name) { + @get:Throws(InterruptedException::class) + val widgetId: Int + get() { + val intent = checkNotNull(blockingGetExtraIntent()) + assertThat(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE).isEqualTo(intent.action) + val widgetId = + intent.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + LauncherAppWidgetInfo.NO_ID, + ) + assertThat(widgetId).isNotEqualTo(LauncherAppWidgetInfo.NO_ID) + return widgetId + } + } +} diff --git a/concurrent/Android.bp b/concurrent/Android.bp new file mode 100644 index 0000000000..81ec210d9f --- /dev/null +++ b/concurrent/Android.bp @@ -0,0 +1,58 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library { + name: "launcher-executor-qualifiers", + srcs: [ + "src/com/android/launcher3/concurrent/annotations/Background.kt", + "src/com/android/launcher3/concurrent/annotations/LightweightBackground.kt", + "src/com/android/launcher3/concurrent/annotations/ThreadPool.kt", + "src/com/android/launcher3/concurrent/annotations/Ui.kt", + "src/com/android/launcher3/concurrent/annotations/UiContext.kt", + "src/com/android/launcher3/concurrent/annotations/BackgroundContext.kt", + "src/com/android/launcher3/concurrent/annotations/LightweightBackgroundContext.kt", + "src/com/android/launcher3/concurrent/annotations/ThreadPoolContext.kt", + ], + sdk_version: "current", + min_sdk_version: min_launcher3_sdk_version, + target_sdk_version: "current", + static_libs: [ + "androidx.annotation_annotation", + "jsr330", + ], +} + +java_library { + name: "launcher-executors-module", + srcs: [ + "src/com/android/launcher3/concurrent/ExecutorsModule.kt", + ], + sdk_version: "current", + min_sdk_version: min_launcher3_sdk_version, + target_sdk_version: "current", + plugins: ["dagger2-compiler"], + static_libs: [ + "androidx.annotation_annotation", + "dagger2", + "guava", + "jsr330", + "launcher-executor-qualifiers", + "kotlinx-coroutines-android", + ], +} diff --git a/concurrent/src/com/android/launcher3/concurrent/ExecutorsModule.kt b/concurrent/src/com/android/launcher3/concurrent/ExecutorsModule.kt new file mode 100644 index 0000000000..c4aefc7732 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/ExecutorsModule.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent + +import com.android.launcher3.concurrent.annotations.Background +import com.android.launcher3.concurrent.annotations.LightweightBackground +import com.android.launcher3.concurrent.annotations.LightweightBackgroundPriority +import com.android.launcher3.concurrent.annotations.ThreadPool +import com.android.launcher3.concurrent.annotations.Ui + +import com.android.launcher3.concurrent.annotations.UiContext +import com.android.launcher3.concurrent.annotations.LightweightBackgroundContext +import com.android.launcher3.concurrent.annotations.BackgroundContext +import com.android.launcher3.concurrent.annotations.ThreadPoolContext + +import com.google.common.util.concurrent.ListeningExecutorService + +import dagger.Binds +import dagger.Module +import dagger.Provides + +import kotlin.coroutines.CoroutineContext +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import javax.inject.Singleton + +import kotlinx.coroutines.asCoroutineDispatcher + +/** + * Module that stipulates the executors that are usable by common launcher + * modules. + * + * This module does not provide the executors directly, but rather provides + * the qualifiers that are used to inject the executors and their derivatives + * when modules themselves need to utilize them. + * + * The executors are provided as follows: + * - @ThreadPool: Provides an unordered background executor or derivative + * backed by a thread pool. + * - @Background: Provides an ordered background executor or derivative. + * - @LightweightBackground: Provides a lightweight background executor or + * derivative that is either time sensitive or not depending on the + * [LightweightBackgroundPriority] flag. + * - @Ui: Provides a UI executor or derivative. + * + * The executors are provided as [Executor], [ExecutorService] or + * [ListeningExecutorService]. + * + * Implementations for the executors should be provided as + * [ListeningExecutorService] by the host. + */ +@Module +interface ExecutorsModule { + + // Unordered background executors + @Binds + @ThreadPool + fun provideThreadPoolExecutor( + @ThreadPool listeningExecutorService: ListeningExecutorService + ): Executor + + @Binds + @ThreadPool + fun provideThreadPoolExecutorService( + @ThreadPool listeningExecutorService: ListeningExecutorService + ): ExecutorService + // end unordered background executors + + // Ordered background executors + @Binds + @Background + fun provideBackgroundExecutor( + @Background listeningExecutorService: ListeningExecutorService + ): Executor + + @Binds + @Background + fun provideBackgroundExecutorService( + @Background listeningExecutorService: ListeningExecutorService + ): ExecutorService + + @Binds + @LightweightBackground(LightweightBackgroundPriority.UI) + fun provideUiLightweightBackgroundExecutor( + @LightweightBackground(LightweightBackgroundPriority.UI) + listeningExecutorService: ListeningExecutorService + ): Executor + + @Binds + @LightweightBackground(LightweightBackgroundPriority.UI) + fun provideUiLightweightBackgroundExecutorService( + @LightweightBackground(LightweightBackgroundPriority.UI) + listeningExecutorService: ListeningExecutorService + ): ExecutorService + + @Binds + @LightweightBackground(LightweightBackgroundPriority.DATA) + fun provideDataLightweightBackgroundExecutor( + @LightweightBackground(LightweightBackgroundPriority.DATA) + listeningExecutorService: ListeningExecutorService + ): Executor + + @Binds + @LightweightBackground(LightweightBackgroundPriority.DATA) + fun provideDataLightweightBackgroundExecutorService( + @LightweightBackground(LightweightBackgroundPriority.DATA) + listeningExecutorService: ListeningExecutorService + ): ExecutorService + // end ordered background executors + + // UI executors + @Binds + @Ui + fun provideUiExecutor( + @Ui listeningExecutorService: ListeningExecutorService + ): Executor + + @Binds + @Ui + fun provideUiExecutorService( + @Ui listeningExecutorService: ListeningExecutorService + ): ExecutorService + // end UI executors + + // The following methods provide the CoroutineContext for the executors. + companion object { + + @Provides + @UiContext + fun provideUiContext( + @Ui executor: Executor + ): CoroutineContext { + return executor.asCoroutineDispatcher() + } + + @Provides + @LightweightBackgroundContext(LightweightBackgroundPriority.DATA) + fun provideDataLightweightContext( + @LightweightBackground(LightweightBackgroundPriority.DATA) + executor: Executor + ): CoroutineContext { + return executor.asCoroutineDispatcher() + } + + @Provides + @LightweightBackgroundContext(LightweightBackgroundPriority.UI) + fun provideUiLightweightContext( + @LightweightBackground(LightweightBackgroundPriority.UI) + executor: Executor + ): CoroutineContext { + return executor.asCoroutineDispatcher() + } + + @Provides + @BackgroundContext + fun provideBackgroundContext( + @Background executor: Executor + ): CoroutineContext { + return executor.asCoroutineDispatcher() + } + + @Provides + @ThreadPoolContext + fun provideThreadPoolContext( + @ThreadPool executor: Executor + ): CoroutineContext { + return executor.asCoroutineDispatcher() + } + } +} \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/Background.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/Background.kt new file mode 100644 index 0000000000..2084568b67 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/Background.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Qualifier for an [Executor] or derivative that runs on a background thread. + * + * This is a background executor that is suitable for common system service + * interactions that could be time consuming. + * + * Usecases include: + * - Sequential system service interactions + * - Sequential I/O interactions + * - etc. + * + * Tasks submitted to this executor are guaranteed to be ordered, but are not + * guaranteed to be executed immediately. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class Background \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/BackgroundContext.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/BackgroundContext.kt new file mode 100644 index 0000000000..799890a32d --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/BackgroundContext.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Qualifier for the [kotlinx.coroutines.CoroutineContext] of the [Background] + * qualifier. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class BackgroundContext \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/LightweightBackground.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/LightweightBackground.kt new file mode 100644 index 0000000000..3c856574a6 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/LightweightBackground.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Priority of the lightweight background executor. + */ +enum class LightweightBackgroundPriority { + + /** + * Priority definition for lightweight background work that is not time + * sensitive, typically for data transformations. + */ + DATA, + + /** + * Priority definition for lightweight background work that is time + * sensitive, typically for user interaction feedback. + */ + UI +} + +/** + * Qualifier for an [Executor] or derivative that runs on a lightweight + * background thread. + * + * This is a lightweight executor that is suitable for running cheap background + * tasks that can either be time sensitive or not depending on the + * [LightweightBackgroundPriority] flag. + * + * LighweightBackground is distinct from Background in that it will not pollute + * the other as to ensure responsiveness of either. + * + * Non priority usecases include: + * - Data transformations within repository layers + * + * Priority usecases include: + * - Responses to user interactions + * + * Tasks submitted to this executor are guaranteed to be ordered. + * + * @param priority The priority of the lightweight background executor. By + * default, this is [LightweightBackgroundPriority.DATA]. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class LightweightBackground( + val priority: LightweightBackgroundPriority = LightweightBackgroundPriority.DATA +) \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/LightweightBackgroundContext.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/LightweightBackgroundContext.kt new file mode 100644 index 0000000000..63f972e7d2 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/LightweightBackgroundContext.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Qualifier for the [kotlinx.coroutines.CoroutineContext] of the + * [LightweightBackground] qualifier. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class LightweightBackgroundContext( + val priority: LightweightBackgroundPriority = LightweightBackgroundPriority.DATA +) \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/ThreadPool.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/ThreadPool.kt new file mode 100644 index 0000000000..cc3b192f32 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/ThreadPool.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Qualifier for an [Executor] or derivative that runs on a background thread + * pool. + * + * This is a background executor that provides execution of tasks that + * are not time sensitive and are time consuming. + * + * Usecases include: + * - Multiple disk I/O interactions + * - Network I/O interactions + * - etc. + * + * Tasks submitted to this executor are not guaranteed to be ordered and may be + * executed immediately or at a later time. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ThreadPool \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/ThreadPoolContext.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/ThreadPoolContext.kt new file mode 100644 index 0000000000..75d0620a92 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/ThreadPoolContext.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Qualifier for the [kotlinx.coroutines.CoroutineContext] of the [ThreadPool] + * qualifier. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ThreadPoolContext \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/Ui.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/Ui.kt new file mode 100644 index 0000000000..5580c428a0 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/Ui.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Qualifier for an [Executor] or derivative that runs on the UI thread. + * + * This is the main thread executor that is suitable for running tasks that are + * time sensitive and are expected to run on the UI thread immediately. + * + * Usecases include: + * - Responsive UI updates + * + * Tasks submitted to this executor are guaranteed to be ordered and executed + * immediately. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class Ui \ No newline at end of file diff --git a/concurrent/src/com/android/launcher3/concurrent/annotations/UiContext.kt b/concurrent/src/com/android/launcher3/concurrent/annotations/UiContext.kt new file mode 100644 index 0000000000..0e1bcb49e1 --- /dev/null +++ b/concurrent/src/com/android/launcher3/concurrent/annotations/UiContext.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.concurrent.annotations + +import javax.inject.Qualifier + +/** + * Qualifier for the [kotlinx.coroutines.CoroutineContext] of the + * [Ui] qualifier. + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class UiContext \ No newline at end of file diff --git a/dagger/Android.bp b/dagger/Android.bp new file mode 100644 index 0000000000..b37e1d60d6 --- /dev/null +++ b/dagger/Android.bp @@ -0,0 +1,31 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library { + name: "launcher-dagger-qualifiers", + srcs: [ + "src/com/android/launcher3/dagger/ActivityContextSingleton.java", + "src/com/android/launcher3/dagger/ApplicationContext.java", + "src/com/android/launcher3/dagger/LauncherAppSingleton.java", + ], + static_libs: [ + "androidx.annotation_annotation", + "jsr330", + ], +} diff --git a/dagger/src/com/android/launcher3/dagger/ActivityContextSingleton.java b/dagger/src/com/android/launcher3/dagger/ActivityContextSingleton.java new file mode 100644 index 0000000000..dd7510cea9 --- /dev/null +++ b/dagger/src/com/android/launcher3/dagger/ActivityContextSingleton.java @@ -0,0 +1,32 @@ +/* + * 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.launcher3.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +/** + * Scope annotation for singletons associated with Launcher activity context. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Scope +public @interface ActivityContextSingleton { +} diff --git a/dagger/src/com/android/launcher3/dagger/ApplicationContext.java b/dagger/src/com/android/launcher3/dagger/ApplicationContext.java new file mode 100644 index 0000000000..9a5b08b51b --- /dev/null +++ b/dagger/src/com/android/launcher3/dagger/ApplicationContext.java @@ -0,0 +1,32 @@ +/* + * 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.launcher3.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * Qualifier for Launcher application context. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +public @interface ApplicationContext { +} diff --git a/dagger/src/com/android/launcher3/dagger/LauncherAppSingleton.java b/dagger/src/com/android/launcher3/dagger/LauncherAppSingleton.java new file mode 100644 index 0000000000..92c00b6d85 --- /dev/null +++ b/dagger/src/com/android/launcher3/dagger/LauncherAppSingleton.java @@ -0,0 +1,32 @@ +/* + * 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.launcher3.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +/** + * Scope annotation for singleton items within the LauncherAppComponent. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Scope +public @interface LauncherAppSingleton { +} diff --git a/docs/assets/README.md b/docs/assets/README.md index 51ef527d05..30e6cb7568 100644 --- a/docs/assets/README.md +++ b/docs/assets/README.md @@ -1,21 +1,19 @@ -# Lawnchair Visual Guidelines +# Lawnchair Assets Guidelines This directory lists all the decoration & visual explainers used in the Lawnchair Documentation. -All assets created should use Material 3 design with `#47B84F` as source color and +All assets created should use Material 3 design with `#47B84F` as source color, and the [Inter](https://fonts.google.com/specimen/Inter) ([OFL v1.1](https://github.com/rsms/inter/?tab=OFL-1.1-1-ov-file#readme)) -typography. Visit the [Material 3 theme builder][material-theme-builder] for more information. +typography. -When creating device mockups for Lawnchair, make sure that you're using the latest commits of -Lawnchair or use Lawnchair Nightly as base. +Visit the [Material 3 theme builder][material-theme-builder] for more information on color. -## Device Mockup +## Device mockup -Use in: [README](/README.md) - -* Icon pack: [Lawnicons](https://github.com/LawnchairLauncher/lawnicons) -* Wallpaper: https://unsplash.com/photos/photography-of-green-leaves-ZVKr8wADhpc -* Color Extraction Technique: Tonal Spot from Lawnchair -* License: https://unsplash.com/license +When creating device mockups for Lawnchair, make sure the latest stable commits of Lawnchair is use +as base. All wallpapers, fonts and icon packs are allowed as long as they're free-to-use +under a permissive license like the licenses from Creative-Common. Using vanilla out-of-the-box +experience is recommended because they show the user what to expect from Lawnchair Launcher when +they first use it. [material-theme-builder]: https://material-foundation.github.io/material-theme-builder/?primary=%2347B84F&bodyFont=Inter&displayFont=Inter&colorMatch=false diff --git a/docs/assets/badge-github.webp b/docs/assets/badge-github.webp new file mode 100644 index 0000000000..591b6b5a51 Binary files /dev/null and b/docs/assets/badge-github.webp differ diff --git a/docs/assets/badge-google-play.webp b/docs/assets/badge-google-play.webp new file mode 100644 index 0000000000..0f0a32ac03 Binary files /dev/null and b/docs/assets/badge-google-play.webp differ diff --git a/docs/assets/badge-izzyondroid.webp b/docs/assets/badge-izzyondroid.webp new file mode 100644 index 0000000000..e9ff948208 Binary files /dev/null and b/docs/assets/badge-izzyondroid.webp differ diff --git a/docs/assets/badge-obtainium.webp b/docs/assets/badge-obtainium.webp new file mode 100644 index 0000000000..2629c812c2 Binary files /dev/null and b/docs/assets/badge-obtainium.webp differ diff --git a/docs/assets/device-frame.webp b/docs/assets/device-frame.webp new file mode 100644 index 0000000000..02e210ba15 Binary files /dev/null and b/docs/assets/device-frame.webp differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index 7af858c97b..7a8ca7d1d3 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -Homescreen based on on Launcher3 from AOSP \ No newline at end of file +Homescreen based on Launcher3 from AOSP diff --git a/flags/README.md b/flags/README.md new file mode 100644 index 0000000000..96d0d9e0ee --- /dev/null +++ b/flags/README.md @@ -0,0 +1,9 @@ +# Launcher3 Flags + +Run + +m Launcher3 + +out\soong\.intermediates\packages\apps\Launcher3\aconfig\com_android_launcher3_flags_lib\android_common\gen\com_android_launcher3_flags_lib.srcjar + +Turn it into JAR and paste the contents into the current Launcher3 flags. diff --git a/flags/build.gradle b/flags/build.gradle index 0dd01c027a..0c48461a59 100644 --- a/flags/build.gradle +++ b/flags/build.gradle @@ -12,5 +12,5 @@ android { } } -addFrameworkJar('framework-15.jar') +addFrameworkJar('framework-16.jar') compileOnlyCommonJars() diff --git a/flags/src/com/android/launcher3/CustomFeatureFlags.java b/flags/src/com/android/launcher3/CustomFeatureFlags.java index e3b74b8dbc..2db6820c48 100644 --- a/flags/src/com/android/launcher3/CustomFeatureFlags.java +++ b/flags/src/com/android/launcher3/CustomFeatureFlags.java @@ -1,12 +1,12 @@ package com.android.launcher3; +// TODO(b/303773055): Remove the annotation after access issue is resolved. import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Predicate; - /** @hide */ public class CustomFeatureFlags implements FeatureFlags { @@ -16,303 +16,807 @@ public class CustomFeatureFlags implements FeatureFlags { mGetValueImpl = getValueImpl; } @Override + + public boolean accessibilityScrollOnAllapps() { + return getValue(Flags.FLAG_ACCESSIBILITY_SCROLL_ON_ALLAPPS, + FeatureFlags::accessibilityScrollOnAllapps); + } + + @Override + + public boolean allAppsBlur() { + return getValue(Flags.FLAG_ALL_APPS_BLUR, + FeatureFlags::allAppsBlur); + } + + @Override + + public boolean allAppsSheetForHandheld() { + return getValue(Flags.FLAG_ALL_APPS_SHEET_FOR_HANDHELD, + FeatureFlags::allAppsSheetForHandheld); + } + + @Override + + public boolean coordinateWorkspaceScale() { + return getValue(Flags.FLAG_COORDINATE_WORKSPACE_SCALE, + FeatureFlags::coordinateWorkspaceScale); + } + + @Override + + public boolean enableActiveGestureProtoLog() { + return getValue(Flags.FLAG_ENABLE_ACTIVE_GESTURE_PROTO_LOG, + FeatureFlags::enableActiveGestureProtoLog); + } + + @Override + public boolean enableAddAppWidgetViaConfigActivityV2() { return getValue(Flags.FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2, - FeatureFlags::enableAddAppWidgetViaConfigActivityV2); + FeatureFlags::enableAddAppWidgetViaConfigActivityV2); } @Override + public boolean enableAdditionalHomeAnimations() { return getValue(Flags.FLAG_ENABLE_ADDITIONAL_HOME_ANIMATIONS, - FeatureFlags::enableAdditionalHomeAnimations); + FeatureFlags::enableAdditionalHomeAnimations); } @Override + + public boolean enableAllAppsButtonInHotseat() { + return getValue(Flags.FLAG_ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT, + FeatureFlags::enableAllAppsButtonInHotseat); + } + + @Override + + public boolean enableAltTabKqsFlatenning() { + return getValue(Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING, + FeatureFlags::enableAltTabKqsFlatenning); + } + + @Override + + public boolean enableAltTabKqsOnConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS, + FeatureFlags::enableAltTabKqsOnConnectedDisplays); + } + + @Override + public boolean enableCategorizedWidgetSuggestions() { return getValue(Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS, - FeatureFlags::enableCategorizedWidgetSuggestions); + FeatureFlags::enableCategorizedWidgetSuggestions); } @Override + + public boolean enableContainerReturnAnimations() { + return getValue(Flags.FLAG_ENABLE_CONTAINER_RETURN_ANIMATIONS, + FeatureFlags::enableContainerReturnAnimations); + } + + @Override + + public boolean enableContrastTiles() { + return getValue(Flags.FLAG_ENABLE_CONTRAST_TILES, + FeatureFlags::enableContrastTiles); + } + + @Override + public boolean enableCursorHoverStates() { return getValue(Flags.FLAG_ENABLE_CURSOR_HOVER_STATES, - FeatureFlags::enableCursorHoverStates); + FeatureFlags::enableCursorHoverStates); } @Override + + public boolean enableDesktopExplodedView() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW, + FeatureFlags::enableDesktopExplodedView); + } + + @Override + + public boolean enableDesktopTaskAlphaAnimation() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_TASK_ALPHA_ANIMATION, + FeatureFlags::enableDesktopTaskAlphaAnimation); + } + + @Override + + public boolean enableDesktopWindowingCarouselDetach() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_CAROUSEL_DETACH, + FeatureFlags::enableDesktopWindowingCarouselDetach); + } + + @Override + + public boolean enableDismissPredictionUndo() { + return getValue(Flags.FLAG_ENABLE_DISMISS_PREDICTION_UNDO, + FeatureFlags::enableDismissPredictionUndo); + } + + @Override + public boolean enableExpandingPauseWorkButton() { return getValue(Flags.FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON, - FeatureFlags::enableExpandingPauseWorkButton); + FeatureFlags::enableExpandingPauseWorkButton); } @Override + + public boolean enableExpressiveDismissTaskMotion() { + return getValue(Flags.FLAG_ENABLE_EXPRESSIVE_DISMISS_TASK_MOTION, + FeatureFlags::enableExpressiveDismissTaskMotion); + } + + @Override + public boolean enableFallbackOverviewInWindow() { return getValue(Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW, - FeatureFlags::enableFallbackOverviewInWindow); + FeatureFlags::enableFallbackOverviewInWindow); } @Override + public boolean enableFirstScreenBroadcastArchivingExtras() { return getValue(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS, - FeatureFlags::enableFirstScreenBroadcastArchivingExtras); + FeatureFlags::enableFirstScreenBroadcastArchivingExtras); } @Override + public boolean enableFocusOutline() { return getValue(Flags.FLAG_ENABLE_FOCUS_OUTLINE, - FeatureFlags::enableFocusOutline); + FeatureFlags::enableFocusOutline); } @Override + public boolean enableGeneratedPreviews() { return getValue(Flags.FLAG_ENABLE_GENERATED_PREVIEWS, - FeatureFlags::enableGeneratedPreviews); + FeatureFlags::enableGeneratedPreviews); } @Override + + public boolean enableGestureNavHorizontalTouchSlop() { + return getValue(Flags.FLAG_ENABLE_GESTURE_NAV_HORIZONTAL_TOUCH_SLOP, + FeatureFlags::enableGestureNavHorizontalTouchSlop); + } + + @Override + + public boolean enableGestureNavOnConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS, + FeatureFlags::enableGestureNavOnConnectedDisplays); + } + + @Override + public boolean enableGridMigrationFix() { return getValue(Flags.FLAG_ENABLE_GRID_MIGRATION_FIX, - FeatureFlags::enableGridMigrationFix); + FeatureFlags::enableGridMigrationFix); } @Override + public boolean enableGridOnlyOverview() { return getValue(Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - FeatureFlags::enableGridOnlyOverview); + FeatureFlags::enableGridOnlyOverview); } @Override + + public boolean enableGrowthNudge() { + return getValue(Flags.FLAG_ENABLE_GROWTH_NUDGE, + FeatureFlags::enableGrowthNudge); + } + + @Override + public boolean enableHandleDelayedGestureCallbacks() { return getValue(Flags.FLAG_ENABLE_HANDLE_DELAYED_GESTURE_CALLBACKS, - FeatureFlags::enableHandleDelayedGestureCallbacks); + FeatureFlags::enableHandleDelayedGestureCallbacks); } @Override + public boolean enableHomeTransitionListener() { return getValue(Flags.FLAG_ENABLE_HOME_TRANSITION_LISTENER, - FeatureFlags::enableHomeTransitionListener); + FeatureFlags::enableHomeTransitionListener); } @Override + + public boolean enableHoverOfChildElementsInTaskview() { + return getValue(Flags.FLAG_ENABLE_HOVER_OF_CHILD_ELEMENTS_IN_TASKVIEW, + FeatureFlags::enableHoverOfChildElementsInTaskview); + } + + @Override + + public boolean enableLargeDesktopWindowingTile() { + return getValue(Flags.FLAG_ENABLE_LARGE_DESKTOP_WINDOWING_TILE, + FeatureFlags::enableLargeDesktopWindowingTile); + } + + @Override + public boolean enableLauncherBrMetricsFixed() { return getValue(Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED, - FeatureFlags::enableLauncherBrMetricsFixed); + FeatureFlags::enableLauncherBrMetricsFixed); } @Override + + public boolean enableLauncherIconShapes() { + return getValue(Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES, + FeatureFlags::enableLauncherIconShapes); + } + + @Override + + public boolean enableLauncherOverviewInWindow() { + return getValue(Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW, + FeatureFlags::enableLauncherOverviewInWindow); + } + + @Override + + public boolean enableLauncherVisualRefresh() { + return getValue(Flags.FLAG_ENABLE_LAUNCHER_VISUAL_REFRESH, + FeatureFlags::enableLauncherVisualRefresh); + } + + @Override + + public boolean enableMouseInteractionChanges() { + return getValue(Flags.FLAG_ENABLE_MOUSE_INTERACTION_CHANGES, + FeatureFlags::enableMouseInteractionChanges); + } + + @Override + + public boolean enableMultiInstanceMenuTaskbar() { + return getValue(Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR, + FeatureFlags::enableMultiInstanceMenuTaskbar); + } + + @Override + public boolean enableNarrowGridRestore() { return getValue(Flags.FLAG_ENABLE_NARROW_GRID_RESTORE, - FeatureFlags::enableNarrowGridRestore); + FeatureFlags::enableNarrowGridRestore); } @Override + + public boolean enableOverviewBackgroundWallpaperBlur() { + return getValue(Flags.FLAG_ENABLE_OVERVIEW_BACKGROUND_WALLPAPER_BLUR, + FeatureFlags::enableOverviewBackgroundWallpaperBlur); + } + + @Override + + public boolean enableOverviewCommandHelperTimeout() { + return getValue(Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT, + FeatureFlags::enableOverviewCommandHelperTimeout); + } + + @Override + + public boolean enableOverviewDesktopTileWallpaperBackground() { + return getValue(Flags.FLAG_ENABLE_OVERVIEW_DESKTOP_TILE_WALLPAPER_BACKGROUND, + FeatureFlags::enableOverviewDesktopTileWallpaperBackground); + } + + @Override + public boolean enableOverviewIconMenu() { return getValue(Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, - FeatureFlags::enableOverviewIconMenu); + FeatureFlags::enableOverviewIconMenu); } @Override + + public boolean enableOverviewOnConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_OVERVIEW_ON_CONNECTED_DISPLAYS, + FeatureFlags::enableOverviewOnConnectedDisplays); + } + + @Override + + public boolean enablePinningAppWithContextMenu() { + return getValue(Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU, + FeatureFlags::enablePinningAppWithContextMenu); + } + + @Override + public boolean enablePredictiveBackGesture() { return getValue(Flags.FLAG_ENABLE_PREDICTIVE_BACK_GESTURE, - FeatureFlags::enablePredictiveBackGesture); + FeatureFlags::enablePredictiveBackGesture); } @Override + public boolean enablePrivateSpace() { return getValue(Flags.FLAG_ENABLE_PRIVATE_SPACE, - FeatureFlags::enablePrivateSpace); + FeatureFlags::enablePrivateSpace); } @Override + public boolean enablePrivateSpaceInstallShortcut() { return getValue(Flags.FLAG_ENABLE_PRIVATE_SPACE_INSTALL_SHORTCUT, - FeatureFlags::enablePrivateSpaceInstallShortcut); + FeatureFlags::enablePrivateSpaceInstallShortcut); } @Override + public boolean enableRebootUnlockAnimation() { return getValue(Flags.FLAG_ENABLE_REBOOT_UNLOCK_ANIMATION, - FeatureFlags::enableRebootUnlockAnimation); + FeatureFlags::enableRebootUnlockAnimation); } @Override + public boolean enableRecentsInTaskbar() { return getValue(Flags.FLAG_ENABLE_RECENTS_IN_TASKBAR, - FeatureFlags::enableRecentsInTaskbar); + FeatureFlags::enableRecentsInTaskbar); } @Override + + public boolean enableRecentsWindowProtoLog() { + return getValue(Flags.FLAG_ENABLE_RECENTS_WINDOW_PROTO_LOG, + FeatureFlags::enableRecentsWindowProtoLog); + } + + @Override + public boolean enableRefactorTaskThumbnail() { return getValue(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, - FeatureFlags::enableRefactorTaskThumbnail); + FeatureFlags::enableRefactorTaskThumbnail); } @Override + public boolean enableResponsiveWorkspace() { return getValue(Flags.FLAG_ENABLE_RESPONSIVE_WORKSPACE, - FeatureFlags::enableResponsiveWorkspace); + FeatureFlags::enableResponsiveWorkspace); } @Override + + public boolean enableScalabilityForDesktopExperience() { + return getValue(Flags.FLAG_ENABLE_SCALABILITY_FOR_DESKTOP_EXPERIENCE, + FeatureFlags::enableScalabilityForDesktopExperience); + } + + @Override + public boolean enableScalingRevealHomeAnimation() { return getValue(Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION, - FeatureFlags::enableScalingRevealHomeAnimation); + FeatureFlags::enableScalingRevealHomeAnimation); } @Override + + public boolean enableSeparateExternalDisplayTasks() { + return getValue(Flags.FLAG_ENABLE_SEPARATE_EXTERNAL_DISPLAY_TASKS, + FeatureFlags::enableSeparateExternalDisplayTasks); + } + + @Override + public boolean enableShortcutDontSuggestApp() { return getValue(Flags.FLAG_ENABLE_SHORTCUT_DONT_SUGGEST_APP, - FeatureFlags::enableShortcutDontSuggestApp); + FeatureFlags::enableShortcutDontSuggestApp); } @Override + + public boolean enableShowEnabledShortcutsInAccessibilityMenu() { + return getValue(Flags.FLAG_ENABLE_SHOW_ENABLED_SHORTCUTS_IN_ACCESSIBILITY_MENU, + FeatureFlags::enableShowEnabledShortcutsInAccessibilityMenu); + } + + @Override + public boolean enableSmartspaceAsAWidget() { return getValue(Flags.FLAG_ENABLE_SMARTSPACE_AS_A_WIDGET, - FeatureFlags::enableSmartspaceAsAWidget); + FeatureFlags::enableSmartspaceAsAWidget); } @Override + public boolean enableSmartspaceRemovalToggle() { return getValue(Flags.FLAG_ENABLE_SMARTSPACE_REMOVAL_TOGGLE, - FeatureFlags::enableSmartspaceRemovalToggle); + FeatureFlags::enableSmartspaceRemovalToggle); } @Override + + public boolean enableStateManagerProtoLog() { + return getValue(Flags.FLAG_ENABLE_STATE_MANAGER_PROTO_LOG, + FeatureFlags::enableStateManagerProtoLog); + } + + @Override + + public boolean enableStrictMode() { + return getValue(Flags.FLAG_ENABLE_STRICT_MODE, + FeatureFlags::enableStrictMode); + } + + @Override + public boolean enableSupportForArchiving() { return getValue(Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING, - FeatureFlags::enableSupportForArchiving); + FeatureFlags::enableSupportForArchiving); } @Override + public boolean enableTabletTwoPanePickerV2() { return getValue(Flags.FLAG_ENABLE_TABLET_TWO_PANE_PICKER_V2, - FeatureFlags::enableTabletTwoPanePickerV2); + FeatureFlags::enableTabletTwoPanePickerV2); } @Override + + public boolean enableTaskbarBehindShade() { + return getValue(Flags.FLAG_ENABLE_TASKBAR_BEHIND_SHADE, + FeatureFlags::enableTaskbarBehindShade); + } + + @Override + public boolean enableTaskbarCustomization() { return getValue(Flags.FLAG_ENABLE_TASKBAR_CUSTOMIZATION, - FeatureFlags::enableTaskbarCustomization); + FeatureFlags::enableTaskbarCustomization); } @Override + + public boolean enableTaskbarForDirectBoot() { + return getValue(Flags.FLAG_ENABLE_TASKBAR_FOR_DIRECT_BOOT, + FeatureFlags::enableTaskbarForDirectBoot); + } + + @Override + public boolean enableTaskbarNoRecreate() { return getValue(Flags.FLAG_ENABLE_TASKBAR_NO_RECREATE, - FeatureFlags::enableTaskbarNoRecreate); + FeatureFlags::enableTaskbarNoRecreate); } @Override + public boolean enableTaskbarPinning() { return getValue(Flags.FLAG_ENABLE_TASKBAR_PINNING, - FeatureFlags::enableTaskbarPinning); + FeatureFlags::enableTaskbarPinning); } @Override + + public boolean enableTieredWidgetsByDefaultInPicker() { + return getValue(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER, + FeatureFlags::enableTieredWidgetsByDefaultInPicker); + } + + @Override + public boolean enableTwoPaneLauncherSettings() { return getValue(Flags.FLAG_ENABLE_TWO_PANE_LAUNCHER_SETTINGS, - FeatureFlags::enableTwoPaneLauncherSettings); + FeatureFlags::enableTwoPaneLauncherSettings); } @Override + public boolean enableTwolineAllapps() { return getValue(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS, - FeatureFlags::enableTwolineAllapps); + FeatureFlags::enableTwolineAllapps); } @Override + public boolean enableTwolineToggle() { return getValue(Flags.FLAG_ENABLE_TWOLINE_TOGGLE, - FeatureFlags::enableTwolineToggle); + FeatureFlags::enableTwolineToggle); } @Override + public boolean enableUnfoldStateAnimation() { return getValue(Flags.FLAG_ENABLE_UNFOLD_STATE_ANIMATION, - FeatureFlags::enableUnfoldStateAnimation); + FeatureFlags::enableUnfoldStateAnimation); } @Override + public boolean enableUnfoldedTwoPanePicker() { return getValue(Flags.FLAG_ENABLE_UNFOLDED_TWO_PANE_PICKER, - FeatureFlags::enableUnfoldedTwoPanePicker); + FeatureFlags::enableUnfoldedTwoPanePicker); } @Override + + public boolean enableUseTopVisibleActivityForExcludeFromRecentTask() { + return getValue(Flags.FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK, + FeatureFlags::enableUseTopVisibleActivityForExcludeFromRecentTask); + } + + @Override + public boolean enableWidgetTapToAdd() { return getValue(Flags.FLAG_ENABLE_WIDGET_TAP_TO_ADD, - FeatureFlags::enableWidgetTapToAdd); + FeatureFlags::enableWidgetTapToAdd); } @Override + public boolean enableWorkspaceInflation() { return getValue(Flags.FLAG_ENABLE_WORKSPACE_INFLATION, - FeatureFlags::enableWorkspaceInflation); + FeatureFlags::enableWorkspaceInflation); } @Override + public boolean enabledFoldersInAllApps() { return getValue(Flags.FLAG_ENABLED_FOLDERS_IN_ALL_APPS, - FeatureFlags::enabledFoldersInAllApps); + FeatureFlags::enabledFoldersInAllApps); } @Override + + public boolean expressiveThemeInTaskbarAndNavigation() { + return getValue(Flags.FLAG_EXPRESSIVE_THEME_IN_TASKBAR_AND_NAVIGATION, + FeatureFlags::expressiveThemeInTaskbarAndNavigation); + } + + @Override + + public boolean extendibleThemeManager() { + return getValue(Flags.FLAG_EXTENDIBLE_THEME_MANAGER, + FeatureFlags::extendibleThemeManager); + } + + @Override + public boolean floatingSearchBar() { return getValue(Flags.FLAG_FLOATING_SEARCH_BAR, - FeatureFlags::floatingSearchBar); + FeatureFlags::floatingSearchBar); } @Override + public boolean forceMonochromeAppIcons() { return getValue(Flags.FLAG_FORCE_MONOCHROME_APP_ICONS, - FeatureFlags::forceMonochromeAppIcons); + FeatureFlags::forceMonochromeAppIcons); } @Override + + public boolean gridMigrationRefactor() { + return getValue(Flags.FLAG_GRID_MIGRATION_REFACTOR, + FeatureFlags::gridMigrationRefactor); + } + + @Override + + public boolean gsfRes() { + return getValue(Flags.FLAG_GSF_RES, + FeatureFlags::gsfRes); + } + + @Override + + public boolean ignoreThreeFingerTrackpadForNavHandleLongPress() { + return getValue(Flags.FLAG_IGNORE_THREE_FINGER_TRACKPAD_FOR_NAV_HANDLE_LONG_PRESS, + FeatureFlags::ignoreThreeFingerTrackpadForNavHandleLongPress); + } + + @Override + + public boolean letterFastScroller() { + return getValue(Flags.FLAG_LETTER_FAST_SCROLLER, + FeatureFlags::letterFastScroller); + } + + @Override + + public boolean msdlFeedback() { + return getValue(Flags.FLAG_MSDL_FEEDBACK, + FeatureFlags::msdlFeedback); + } + + @Override + + public boolean multilineSearchBar() { + return getValue(Flags.FLAG_MULTILINE_SEARCH_BAR, + FeatureFlags::multilineSearchBar); + } + + @Override + + public boolean navigateToChildPreference() { + return getValue(Flags.FLAG_NAVIGATE_TO_CHILD_PREFERENCE, + FeatureFlags::navigateToChildPreference); + } + + @Override + + public boolean oneGridMountedMode() { + return getValue(Flags.FLAG_ONE_GRID_MOUNTED_MODE, + FeatureFlags::oneGridMountedMode); + } + + @Override + + public boolean oneGridRotationHandling() { + return getValue(Flags.FLAG_ONE_GRID_ROTATION_HANDLING, + FeatureFlags::oneGridRotationHandling); + } + + @Override + + public boolean oneGridSpecs() { + return getValue(Flags.FLAG_ONE_GRID_SPECS, + FeatureFlags::oneGridSpecs); + } + + @Override + + public boolean predictiveBackToHomeBlur() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_TO_HOME_BLUR, + FeatureFlags::predictiveBackToHomeBlur); + } + + @Override + + public boolean predictiveBackToHomePolish() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_TO_HOME_POLISH, + FeatureFlags::predictiveBackToHomePolish); + } + + @Override + public boolean privateSpaceAddFloatingMaskView() { return getValue(Flags.FLAG_PRIVATE_SPACE_ADD_FLOATING_MASK_VIEW, - FeatureFlags::privateSpaceAddFloatingMaskView); + FeatureFlags::privateSpaceAddFloatingMaskView); } @Override + public boolean privateSpaceAnimation() { return getValue(Flags.FLAG_PRIVATE_SPACE_ANIMATION, - FeatureFlags::privateSpaceAnimation); + FeatureFlags::privateSpaceAnimation); } @Override + public boolean privateSpaceAppInstallerButton() { return getValue(Flags.FLAG_PRIVATE_SPACE_APP_INSTALLER_BUTTON, - FeatureFlags::privateSpaceAppInstallerButton); + FeatureFlags::privateSpaceAppInstallerButton); } @Override + public boolean privateSpaceRestrictAccessibilityDrag() { return getValue(Flags.FLAG_PRIVATE_SPACE_RESTRICT_ACCESSIBILITY_DRAG, - FeatureFlags::privateSpaceRestrictAccessibilityDrag); + FeatureFlags::privateSpaceRestrictAccessibilityDrag); } @Override + public boolean privateSpaceRestrictItemDrag() { return getValue(Flags.FLAG_PRIVATE_SPACE_RESTRICT_ITEM_DRAG, - FeatureFlags::privateSpaceRestrictItemDrag); + FeatureFlags::privateSpaceRestrictItemDrag); } @Override + public boolean privateSpaceSysAppsSeparation() { return getValue(Flags.FLAG_PRIVATE_SPACE_SYS_APPS_SEPARATION, - FeatureFlags::privateSpaceSysAppsSeparation); + FeatureFlags::privateSpaceSysAppsSeparation); } @Override + + public boolean removeAppsRefreshOnRightClick() { + return getValue(Flags.FLAG_REMOVE_APPS_REFRESH_ON_RIGHT_CLICK, + FeatureFlags::removeAppsRefreshOnRightClick); + } + + @Override + + public boolean removeExcludeFromScreenMagnificationFlagUsage() { + return getValue(Flags.FLAG_REMOVE_EXCLUDE_FROM_SCREEN_MAGNIFICATION_FLAG_USAGE, + FeatureFlags::removeExcludeFromScreenMagnificationFlagUsage); + } + + @Override + + public boolean restoreArchivedAppIconsFromDb() { + return getValue(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB, + FeatureFlags::restoreArchivedAppIconsFromDb); + } + + @Override + + public boolean restoreArchivedShortcuts() { + return getValue(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS, + FeatureFlags::restoreArchivedShortcuts); + } + + @Override + + public boolean showTaskbarPinningPopupFromAnywhere() { + return getValue(Flags.FLAG_SHOW_TASKBAR_PINNING_POPUP_FROM_ANYWHERE, + FeatureFlags::showTaskbarPinningPopupFromAnywhere); + } + + @Override + + public boolean syncAppLaunchWithTaskbarStash() { + return getValue(Flags.FLAG_SYNC_APP_LAUNCH_WITH_TASKBAR_STASH, + FeatureFlags::syncAppLaunchWithTaskbarStash); + } + + @Override + + public boolean taskbarOverflow() { + return getValue(Flags.FLAG_TASKBAR_OVERFLOW, + FeatureFlags::taskbarOverflow); + } + + @Override + + public boolean taskbarQuietModeChangeSupport() { + return getValue(Flags.FLAG_TASKBAR_QUIET_MODE_CHANGE_SUPPORT, + FeatureFlags::taskbarQuietModeChangeSupport); + } + + @Override + public boolean useActivityOverlay() { return getValue(Flags.FLAG_USE_ACTIVITY_OVERLAY, - FeatureFlags::useActivityOverlay); + FeatureFlags::useActivityOverlay); + } + + @Override + + public boolean useNewIconForArchivedApps() { + return getValue(Flags.FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS, + FeatureFlags::useNewIconForArchivedApps); + } + + @Override + + public boolean useSystemRadiusForAppWidgets() { + return getValue(Flags.FLAG_USE_SYSTEM_RADIUS_FOR_APP_WIDGETS, + FeatureFlags::useSystemRadiusForAppWidgets); + } + + @Override + + public boolean workSchedulerInWorkProfile() { + return getValue(Flags.FLAG_WORK_SCHEDULER_IN_WORK_PROFILE, + FeatureFlags::workSchedulerInWorkProfile); } public boolean isFlagReadOnlyOptimized(String flagName) { if (mReadOnlyFlagsSet.contains(flagName) && - isOptimizationEnabled()) { - return true; + isOptimizationEnabled()) { + return true; } return false; } @@ -327,65 +831,240 @@ public class CustomFeatureFlags implements FeatureFlags { public List getFlagNames() { return Arrays.asList( - Flags.FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2, - Flags.FLAG_ENABLE_ADDITIONAL_HOME_ANIMATIONS, - Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS, - Flags.FLAG_ENABLE_CURSOR_HOVER_STATES, - Flags.FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON, - Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW, - Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS, - Flags.FLAG_ENABLE_FOCUS_OUTLINE, - Flags.FLAG_ENABLE_GENERATED_PREVIEWS, - Flags.FLAG_ENABLE_GRID_MIGRATION_FIX, - Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_HANDLE_DELAYED_GESTURE_CALLBACKS, - Flags.FLAG_ENABLE_HOME_TRANSITION_LISTENER, - Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED, - Flags.FLAG_ENABLE_NARROW_GRID_RESTORE, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, - Flags.FLAG_ENABLE_PREDICTIVE_BACK_GESTURE, - Flags.FLAG_ENABLE_PRIVATE_SPACE, - Flags.FLAG_ENABLE_PRIVATE_SPACE_INSTALL_SHORTCUT, - Flags.FLAG_ENABLE_REBOOT_UNLOCK_ANIMATION, - Flags.FLAG_ENABLE_RECENTS_IN_TASKBAR, - Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, - Flags.FLAG_ENABLE_RESPONSIVE_WORKSPACE, - Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION, - Flags.FLAG_ENABLE_SHORTCUT_DONT_SUGGEST_APP, - Flags.FLAG_ENABLE_SMARTSPACE_AS_A_WIDGET, - Flags.FLAG_ENABLE_SMARTSPACE_REMOVAL_TOGGLE, - Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING, - Flags.FLAG_ENABLE_TABLET_TWO_PANE_PICKER_V2, - Flags.FLAG_ENABLE_TASKBAR_CUSTOMIZATION, - Flags.FLAG_ENABLE_TASKBAR_NO_RECREATE, - Flags.FLAG_ENABLE_TASKBAR_PINNING, - Flags.FLAG_ENABLE_TWO_PANE_LAUNCHER_SETTINGS, - Flags.FLAG_ENABLE_TWOLINE_ALLAPPS, - Flags.FLAG_ENABLE_TWOLINE_TOGGLE, - Flags.FLAG_ENABLE_UNFOLD_STATE_ANIMATION, - Flags.FLAG_ENABLE_UNFOLDED_TWO_PANE_PICKER, - Flags.FLAG_ENABLE_WIDGET_TAP_TO_ADD, - Flags.FLAG_ENABLE_WORKSPACE_INFLATION, - Flags.FLAG_ENABLED_FOLDERS_IN_ALL_APPS, - Flags.FLAG_FLOATING_SEARCH_BAR, - Flags.FLAG_FORCE_MONOCHROME_APP_ICONS, - Flags.FLAG_PRIVATE_SPACE_ADD_FLOATING_MASK_VIEW, - Flags.FLAG_PRIVATE_SPACE_ANIMATION, - Flags.FLAG_PRIVATE_SPACE_APP_INSTALLER_BUTTON, - Flags.FLAG_PRIVATE_SPACE_RESTRICT_ACCESSIBILITY_DRAG, - Flags.FLAG_PRIVATE_SPACE_RESTRICT_ITEM_DRAG, - Flags.FLAG_PRIVATE_SPACE_SYS_APPS_SEPARATION, - Flags.FLAG_USE_ACTIVITY_OVERLAY + Flags.FLAG_ACCESSIBILITY_SCROLL_ON_ALLAPPS, + Flags.FLAG_ALL_APPS_BLUR, + Flags.FLAG_ALL_APPS_SHEET_FOR_HANDHELD, + Flags.FLAG_COORDINATE_WORKSPACE_SCALE, + Flags.FLAG_ENABLE_ACTIVE_GESTURE_PROTO_LOG, + Flags.FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2, + Flags.FLAG_ENABLE_ADDITIONAL_HOME_ANIMATIONS, + Flags.FLAG_ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT, + Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING, + Flags.FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS, + Flags.FLAG_ENABLE_CONTAINER_RETURN_ANIMATIONS, + Flags.FLAG_ENABLE_CONTRAST_TILES, + Flags.FLAG_ENABLE_CURSOR_HOVER_STATES, + Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW, + Flags.FLAG_ENABLE_DESKTOP_TASK_ALPHA_ANIMATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_CAROUSEL_DETACH, + Flags.FLAG_ENABLE_DISMISS_PREDICTION_UNDO, + Flags.FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON, + Flags.FLAG_ENABLE_EXPRESSIVE_DISMISS_TASK_MOTION, + Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW, + Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS, + Flags.FLAG_ENABLE_FOCUS_OUTLINE, + Flags.FLAG_ENABLE_GENERATED_PREVIEWS, + Flags.FLAG_ENABLE_GESTURE_NAV_HORIZONTAL_TOUCH_SLOP, + Flags.FLAG_ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_GRID_MIGRATION_FIX, + Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, + Flags.FLAG_ENABLE_GROWTH_NUDGE, + Flags.FLAG_ENABLE_HANDLE_DELAYED_GESTURE_CALLBACKS, + Flags.FLAG_ENABLE_HOME_TRANSITION_LISTENER, + Flags.FLAG_ENABLE_HOVER_OF_CHILD_ELEMENTS_IN_TASKVIEW, + Flags.FLAG_ENABLE_LARGE_DESKTOP_WINDOWING_TILE, + Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED, + Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES, + Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW, + Flags.FLAG_ENABLE_LAUNCHER_VISUAL_REFRESH, + Flags.FLAG_ENABLE_MOUSE_INTERACTION_CHANGES, + Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR, + Flags.FLAG_ENABLE_NARROW_GRID_RESTORE, + Flags.FLAG_ENABLE_OVERVIEW_BACKGROUND_WALLPAPER_BLUR, + Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT, + Flags.FLAG_ENABLE_OVERVIEW_DESKTOP_TILE_WALLPAPER_BACKGROUND, + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, + Flags.FLAG_ENABLE_OVERVIEW_ON_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU, + Flags.FLAG_ENABLE_PREDICTIVE_BACK_GESTURE, + Flags.FLAG_ENABLE_PRIVATE_SPACE, + Flags.FLAG_ENABLE_PRIVATE_SPACE_INSTALL_SHORTCUT, + Flags.FLAG_ENABLE_REBOOT_UNLOCK_ANIMATION, + Flags.FLAG_ENABLE_RECENTS_IN_TASKBAR, + Flags.FLAG_ENABLE_RECENTS_WINDOW_PROTO_LOG, + Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, + Flags.FLAG_ENABLE_RESPONSIVE_WORKSPACE, + Flags.FLAG_ENABLE_SCALABILITY_FOR_DESKTOP_EXPERIENCE, + Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION, + Flags.FLAG_ENABLE_SEPARATE_EXTERNAL_DISPLAY_TASKS, + Flags.FLAG_ENABLE_SHORTCUT_DONT_SUGGEST_APP, + Flags.FLAG_ENABLE_SHOW_ENABLED_SHORTCUTS_IN_ACCESSIBILITY_MENU, + Flags.FLAG_ENABLE_SMARTSPACE_AS_A_WIDGET, + Flags.FLAG_ENABLE_SMARTSPACE_REMOVAL_TOGGLE, + Flags.FLAG_ENABLE_STATE_MANAGER_PROTO_LOG, + Flags.FLAG_ENABLE_STRICT_MODE, + Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING, + Flags.FLAG_ENABLE_TABLET_TWO_PANE_PICKER_V2, + Flags.FLAG_ENABLE_TASKBAR_BEHIND_SHADE, + Flags.FLAG_ENABLE_TASKBAR_CUSTOMIZATION, + Flags.FLAG_ENABLE_TASKBAR_FOR_DIRECT_BOOT, + Flags.FLAG_ENABLE_TASKBAR_NO_RECREATE, + Flags.FLAG_ENABLE_TASKBAR_PINNING, + Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER, + Flags.FLAG_ENABLE_TWO_PANE_LAUNCHER_SETTINGS, + Flags.FLAG_ENABLE_TWOLINE_ALLAPPS, + Flags.FLAG_ENABLE_TWOLINE_TOGGLE, + Flags.FLAG_ENABLE_UNFOLD_STATE_ANIMATION, + Flags.FLAG_ENABLE_UNFOLDED_TWO_PANE_PICKER, + Flags.FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK, + Flags.FLAG_ENABLE_WIDGET_TAP_TO_ADD, + Flags.FLAG_ENABLE_WORKSPACE_INFLATION, + Flags.FLAG_ENABLED_FOLDERS_IN_ALL_APPS, + Flags.FLAG_EXPRESSIVE_THEME_IN_TASKBAR_AND_NAVIGATION, + Flags.FLAG_EXTENDIBLE_THEME_MANAGER, + Flags.FLAG_FLOATING_SEARCH_BAR, + Flags.FLAG_FORCE_MONOCHROME_APP_ICONS, + Flags.FLAG_GRID_MIGRATION_REFACTOR, + Flags.FLAG_GSF_RES, + Flags.FLAG_IGNORE_THREE_FINGER_TRACKPAD_FOR_NAV_HANDLE_LONG_PRESS, + Flags.FLAG_LETTER_FAST_SCROLLER, + Flags.FLAG_MSDL_FEEDBACK, + Flags.FLAG_MULTILINE_SEARCH_BAR, + Flags.FLAG_NAVIGATE_TO_CHILD_PREFERENCE, + Flags.FLAG_ONE_GRID_MOUNTED_MODE, + Flags.FLAG_ONE_GRID_ROTATION_HANDLING, + Flags.FLAG_ONE_GRID_SPECS, + Flags.FLAG_PREDICTIVE_BACK_TO_HOME_BLUR, + Flags.FLAG_PREDICTIVE_BACK_TO_HOME_POLISH, + Flags.FLAG_PRIVATE_SPACE_ADD_FLOATING_MASK_VIEW, + Flags.FLAG_PRIVATE_SPACE_ANIMATION, + Flags.FLAG_PRIVATE_SPACE_APP_INSTALLER_BUTTON, + Flags.FLAG_PRIVATE_SPACE_RESTRICT_ACCESSIBILITY_DRAG, + Flags.FLAG_PRIVATE_SPACE_RESTRICT_ITEM_DRAG, + Flags.FLAG_PRIVATE_SPACE_SYS_APPS_SEPARATION, + Flags.FLAG_REMOVE_APPS_REFRESH_ON_RIGHT_CLICK, + Flags.FLAG_REMOVE_EXCLUDE_FROM_SCREEN_MAGNIFICATION_FLAG_USAGE, + Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB, + Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS, + Flags.FLAG_SHOW_TASKBAR_PINNING_POPUP_FROM_ANYWHERE, + Flags.FLAG_SYNC_APP_LAUNCH_WITH_TASKBAR_STASH, + Flags.FLAG_TASKBAR_OVERFLOW, + Flags.FLAG_TASKBAR_QUIET_MODE_CHANGE_SUPPORT, + Flags.FLAG_USE_ACTIVITY_OVERLAY, + Flags.FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS, + Flags.FLAG_USE_SYSTEM_RADIUS_FOR_APP_WIDGETS, + Flags.FLAG_WORK_SCHEDULER_IN_WORK_PROFILE ); } private Set mReadOnlyFlagsSet = new HashSet<>( - Arrays.asList( - Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS, - Flags.FLAG_ENABLE_GRID_MIGRATION_FIX, - Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED, - Flags.FLAG_ENABLE_NARROW_GRID_RESTORE, - "" - ) + Arrays.asList( + Flags.FLAG_ACCESSIBILITY_SCROLL_ON_ALLAPPS, + Flags.FLAG_ALL_APPS_BLUR, + Flags.FLAG_ALL_APPS_SHEET_FOR_HANDHELD, + Flags.FLAG_COORDINATE_WORKSPACE_SCALE, + Flags.FLAG_ENABLE_ACTIVE_GESTURE_PROTO_LOG, + Flags.FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2, + Flags.FLAG_ENABLE_ADDITIONAL_HOME_ANIMATIONS, + Flags.FLAG_ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT, + Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING, + Flags.FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS, + Flags.FLAG_ENABLE_CONTAINER_RETURN_ANIMATIONS, + Flags.FLAG_ENABLE_CONTRAST_TILES, + Flags.FLAG_ENABLE_CURSOR_HOVER_STATES, + Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW, + Flags.FLAG_ENABLE_DESKTOP_TASK_ALPHA_ANIMATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_CAROUSEL_DETACH, + Flags.FLAG_ENABLE_DISMISS_PREDICTION_UNDO, + Flags.FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON, + Flags.FLAG_ENABLE_EXPRESSIVE_DISMISS_TASK_MOTION, + Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW, + Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS, + Flags.FLAG_ENABLE_FOCUS_OUTLINE, + Flags.FLAG_ENABLE_GENERATED_PREVIEWS, + Flags.FLAG_ENABLE_GESTURE_NAV_HORIZONTAL_TOUCH_SLOP, + Flags.FLAG_ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_GRID_MIGRATION_FIX, + Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, + Flags.FLAG_ENABLE_GROWTH_NUDGE, + Flags.FLAG_ENABLE_HANDLE_DELAYED_GESTURE_CALLBACKS, + Flags.FLAG_ENABLE_HOME_TRANSITION_LISTENER, + Flags.FLAG_ENABLE_HOVER_OF_CHILD_ELEMENTS_IN_TASKVIEW, + Flags.FLAG_ENABLE_LARGE_DESKTOP_WINDOWING_TILE, + Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED, + Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES, + Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW, + Flags.FLAG_ENABLE_LAUNCHER_VISUAL_REFRESH, + Flags.FLAG_ENABLE_MOUSE_INTERACTION_CHANGES, + Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR, + Flags.FLAG_ENABLE_NARROW_GRID_RESTORE, + Flags.FLAG_ENABLE_OVERVIEW_BACKGROUND_WALLPAPER_BLUR, + Flags.FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT, + Flags.FLAG_ENABLE_OVERVIEW_DESKTOP_TILE_WALLPAPER_BACKGROUND, + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, + Flags.FLAG_ENABLE_OVERVIEW_ON_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU, + Flags.FLAG_ENABLE_PREDICTIVE_BACK_GESTURE, + Flags.FLAG_ENABLE_PRIVATE_SPACE, + Flags.FLAG_ENABLE_PRIVATE_SPACE_INSTALL_SHORTCUT, + Flags.FLAG_ENABLE_REBOOT_UNLOCK_ANIMATION, + Flags.FLAG_ENABLE_RECENTS_IN_TASKBAR, + Flags.FLAG_ENABLE_RECENTS_WINDOW_PROTO_LOG, + Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, + Flags.FLAG_ENABLE_RESPONSIVE_WORKSPACE, + Flags.FLAG_ENABLE_SCALABILITY_FOR_DESKTOP_EXPERIENCE, + Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION, + Flags.FLAG_ENABLE_SEPARATE_EXTERNAL_DISPLAY_TASKS, + Flags.FLAG_ENABLE_SHORTCUT_DONT_SUGGEST_APP, + Flags.FLAG_ENABLE_SHOW_ENABLED_SHORTCUTS_IN_ACCESSIBILITY_MENU, + Flags.FLAG_ENABLE_SMARTSPACE_AS_A_WIDGET, + Flags.FLAG_ENABLE_SMARTSPACE_REMOVAL_TOGGLE, + Flags.FLAG_ENABLE_STATE_MANAGER_PROTO_LOG, + Flags.FLAG_ENABLE_STRICT_MODE, + Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING, + Flags.FLAG_ENABLE_TABLET_TWO_PANE_PICKER_V2, + Flags.FLAG_ENABLE_TASKBAR_BEHIND_SHADE, + Flags.FLAG_ENABLE_TASKBAR_CUSTOMIZATION, + Flags.FLAG_ENABLE_TASKBAR_FOR_DIRECT_BOOT, + Flags.FLAG_ENABLE_TASKBAR_NO_RECREATE, + Flags.FLAG_ENABLE_TASKBAR_PINNING, + Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER, + Flags.FLAG_ENABLE_TWO_PANE_LAUNCHER_SETTINGS, + Flags.FLAG_ENABLE_TWOLINE_ALLAPPS, + Flags.FLAG_ENABLE_TWOLINE_TOGGLE, + Flags.FLAG_ENABLE_UNFOLD_STATE_ANIMATION, + Flags.FLAG_ENABLE_UNFOLDED_TWO_PANE_PICKER, + Flags.FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK, + Flags.FLAG_ENABLE_WIDGET_TAP_TO_ADD, + Flags.FLAG_ENABLE_WORKSPACE_INFLATION, + Flags.FLAG_ENABLED_FOLDERS_IN_ALL_APPS, + Flags.FLAG_EXPRESSIVE_THEME_IN_TASKBAR_AND_NAVIGATION, + Flags.FLAG_EXTENDIBLE_THEME_MANAGER, + Flags.FLAG_FLOATING_SEARCH_BAR, + Flags.FLAG_FORCE_MONOCHROME_APP_ICONS, + Flags.FLAG_GRID_MIGRATION_REFACTOR, + Flags.FLAG_GSF_RES, + Flags.FLAG_IGNORE_THREE_FINGER_TRACKPAD_FOR_NAV_HANDLE_LONG_PRESS, + Flags.FLAG_LETTER_FAST_SCROLLER, + Flags.FLAG_MSDL_FEEDBACK, + Flags.FLAG_MULTILINE_SEARCH_BAR, + Flags.FLAG_NAVIGATE_TO_CHILD_PREFERENCE, + Flags.FLAG_ONE_GRID_MOUNTED_MODE, + Flags.FLAG_ONE_GRID_ROTATION_HANDLING, + Flags.FLAG_ONE_GRID_SPECS, + Flags.FLAG_PREDICTIVE_BACK_TO_HOME_BLUR, + Flags.FLAG_PREDICTIVE_BACK_TO_HOME_POLISH, + Flags.FLAG_PRIVATE_SPACE_ADD_FLOATING_MASK_VIEW, + Flags.FLAG_PRIVATE_SPACE_ANIMATION, + Flags.FLAG_PRIVATE_SPACE_APP_INSTALLER_BUTTON, + Flags.FLAG_PRIVATE_SPACE_RESTRICT_ACCESSIBILITY_DRAG, + Flags.FLAG_PRIVATE_SPACE_RESTRICT_ITEM_DRAG, + Flags.FLAG_PRIVATE_SPACE_SYS_APPS_SEPARATION, + Flags.FLAG_REMOVE_APPS_REFRESH_ON_RIGHT_CLICK, + Flags.FLAG_REMOVE_EXCLUDE_FROM_SCREEN_MAGNIFICATION_FLAG_USAGE, + Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB, + Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS, + Flags.FLAG_SHOW_TASKBAR_PINNING_POPUP_FROM_ANYWHERE, + Flags.FLAG_SYNC_APP_LAUNCH_WITH_TASKBAR_STASH, + Flags.FLAG_TASKBAR_OVERFLOW, + Flags.FLAG_TASKBAR_QUIET_MODE_CHANGE_SUPPORT, + Flags.FLAG_USE_ACTIVITY_OVERLAY, + Flags.FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS, + Flags.FLAG_USE_SYSTEM_RADIUS_FOR_APP_WIDGETS, + Flags.FLAG_WORK_SCHEDULER_IN_WORK_PROFILE, + "" + ) ); } diff --git a/flags/src/com/android/launcher3/FakeFeatureFlagsImpl.java b/flags/src/com/android/launcher3/FakeFeatureFlagsImpl.java index 1dc6e623ae..1416ee2007 100644 --- a/flags/src/com/android/launcher3/FakeFeatureFlagsImpl.java +++ b/flags/src/com/android/launcher3/FakeFeatureFlagsImpl.java @@ -3,7 +3,6 @@ package com.android.launcher3; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; - /** @hide */ public class FakeFeatureFlagsImpl extends CustomFeatureFlags { private final Map mFlagMap = new HashMap<>(); diff --git a/flags/src/com/android/launcher3/FeatureFlags.java b/flags/src/com/android/launcher3/FeatureFlags.java index 7cd0b034f2..6c7a84557f 100644 --- a/flags/src/com/android/launcher3/FeatureFlags.java +++ b/flags/src/com/android/launcher3/FeatureFlags.java @@ -1,103 +1,348 @@ package com.android.launcher3; +// TODO(b/303773055): Remove the annotation after access issue is resolved. /** @hide */ public interface FeatureFlags { + + boolean accessibilityScrollOnAllapps(); + + + boolean allAppsBlur(); + + + boolean allAppsSheetForHandheld(); + + + boolean coordinateWorkspaceScale(); + + + boolean enableActiveGestureProtoLog(); + + boolean enableAddAppWidgetViaConfigActivityV2(); + boolean enableAdditionalHomeAnimations(); + + boolean enableAllAppsButtonInHotseat(); + + + boolean enableAltTabKqsFlatenning(); + + + boolean enableAltTabKqsOnConnectedDisplays(); + + boolean enableCategorizedWidgetSuggestions(); + + boolean enableContainerReturnAnimations(); + + + boolean enableContrastTiles(); + + boolean enableCursorHoverStates(); + + boolean enableDesktopExplodedView(); + + + boolean enableDesktopTaskAlphaAnimation(); + + + boolean enableDesktopWindowingCarouselDetach(); + + + boolean enableDismissPredictionUndo(); + + boolean enableExpandingPauseWorkButton(); + + boolean enableExpressiveDismissTaskMotion(); + + boolean enableFallbackOverviewInWindow(); + boolean enableFirstScreenBroadcastArchivingExtras(); + boolean enableFocusOutline(); + boolean enableGeneratedPreviews(); + + boolean enableGestureNavHorizontalTouchSlop(); + + + boolean enableGestureNavOnConnectedDisplays(); + + boolean enableGridMigrationFix(); + boolean enableGridOnlyOverview(); + + boolean enableGrowthNudge(); + + boolean enableHandleDelayedGestureCallbacks(); + boolean enableHomeTransitionListener(); + + boolean enableHoverOfChildElementsInTaskview(); + + + boolean enableLargeDesktopWindowingTile(); + + boolean enableLauncherBrMetricsFixed(); + + boolean enableLauncherIconShapes(); + + + boolean enableLauncherOverviewInWindow(); + + + boolean enableLauncherVisualRefresh(); + + + boolean enableMouseInteractionChanges(); + + + boolean enableMultiInstanceMenuTaskbar(); + + boolean enableNarrowGridRestore(); + + boolean enableOverviewBackgroundWallpaperBlur(); + + + boolean enableOverviewCommandHelperTimeout(); + + + boolean enableOverviewDesktopTileWallpaperBackground(); + + boolean enableOverviewIconMenu(); + + boolean enableOverviewOnConnectedDisplays(); + + + boolean enablePinningAppWithContextMenu(); + + boolean enablePredictiveBackGesture(); + boolean enablePrivateSpace(); + boolean enablePrivateSpaceInstallShortcut(); + boolean enableRebootUnlockAnimation(); + boolean enableRecentsInTaskbar(); + + boolean enableRecentsWindowProtoLog(); + + boolean enableRefactorTaskThumbnail(); + boolean enableResponsiveWorkspace(); + + boolean enableScalabilityForDesktopExperience(); + + boolean enableScalingRevealHomeAnimation(); + + boolean enableSeparateExternalDisplayTasks(); + + boolean enableShortcutDontSuggestApp(); + + boolean enableShowEnabledShortcutsInAccessibilityMenu(); + + boolean enableSmartspaceAsAWidget(); + boolean enableSmartspaceRemovalToggle(); + + boolean enableStateManagerProtoLog(); + + + boolean enableStrictMode(); + + boolean enableSupportForArchiving(); + boolean enableTabletTwoPanePickerV2(); + + boolean enableTaskbarBehindShade(); + + boolean enableTaskbarCustomization(); + + boolean enableTaskbarForDirectBoot(); + + boolean enableTaskbarNoRecreate(); + boolean enableTaskbarPinning(); + + boolean enableTieredWidgetsByDefaultInPicker(); + + boolean enableTwoPaneLauncherSettings(); + boolean enableTwolineAllapps(); + boolean enableTwolineToggle(); + boolean enableUnfoldStateAnimation(); + boolean enableUnfoldedTwoPanePicker(); + + boolean enableUseTopVisibleActivityForExcludeFromRecentTask(); + + boolean enableWidgetTapToAdd(); + boolean enableWorkspaceInflation(); + boolean enabledFoldersInAllApps(); + + boolean expressiveThemeInTaskbarAndNavigation(); + + + boolean extendibleThemeManager(); + + boolean floatingSearchBar(); + boolean forceMonochromeAppIcons(); + + boolean gridMigrationRefactor(); + + + boolean gsfRes(); + + + boolean ignoreThreeFingerTrackpadForNavHandleLongPress(); + + + boolean letterFastScroller(); + + + boolean msdlFeedback(); + + + boolean multilineSearchBar(); + + + boolean navigateToChildPreference(); + + + boolean oneGridMountedMode(); + + + boolean oneGridRotationHandling(); + + + boolean oneGridSpecs(); + + + boolean predictiveBackToHomeBlur(); + + + boolean predictiveBackToHomePolish(); + + boolean privateSpaceAddFloatingMaskView(); + boolean privateSpaceAnimation(); + boolean privateSpaceAppInstallerButton(); + boolean privateSpaceRestrictAccessibilityDrag(); + boolean privateSpaceRestrictItemDrag(); + boolean privateSpaceSysAppsSeparation(); + + boolean removeAppsRefreshOnRightClick(); + + + boolean removeExcludeFromScreenMagnificationFlagUsage(); + + + boolean restoreArchivedAppIconsFromDb(); + + + boolean restoreArchivedShortcuts(); + + + boolean showTaskbarPinningPopupFromAnywhere(); + + + boolean syncAppLaunchWithTaskbarStash(); + + + boolean taskbarOverflow(); + + + boolean taskbarQuietModeChangeSupport(); + + boolean useActivityOverlay(); -} \ No newline at end of file + + + boolean useNewIconForArchivedApps(); + + + boolean useSystemRadiusForAppWidgets(); + + + boolean workSchedulerInWorkProfile(); +} diff --git a/flags/src/com/android/launcher3/FeatureFlagsImpl.java b/flags/src/com/android/launcher3/FeatureFlagsImpl.java index d0aad19aaa..1fd17137d9 100644 --- a/flags/src/com/android/launcher3/FeatureFlagsImpl.java +++ b/flags/src/com/android/launcher3/FeatureFlagsImpl.java @@ -1,880 +1,803 @@ package com.android.launcher3; // TODO(b/303773055): Remove the annotation after access issue is resolved. -import androidx.core.os.BuildCompat; - -import com.android.quickstep.util.DeviceConfigHelper; - -import java.nio.file.Files; -import java.nio.file.Paths; /** @hide */ public final class FeatureFlagsImpl implements FeatureFlags { - private static final boolean isReadFromNew = Files.exists(Paths.get("/metadata/aconfig/boot/enable_only_new_storage")); - private static volatile boolean isCached = false; - private static volatile boolean launcher_is_cached = false; - private static volatile boolean launcher_search_is_cached = false; - private static boolean enableAddAppWidgetViaConfigActivityV2 = true; - private static boolean enableAdditionalHomeAnimations = true; - private static boolean enableCategorizedWidgetSuggestions = true; - private static boolean enableCursorHoverStates = true; - private static boolean enableExpandingPauseWorkButton = true; - private static boolean enableFallbackOverviewInWindow = false; - private static boolean enableFocusOutline = true; - private static boolean enableGeneratedPreviews = true; - private static boolean enableGridOnlyOverview = false; - private static boolean enableHandleDelayedGestureCallbacks = true; - private static boolean enableHomeTransitionListener = true; - private static boolean enableOverviewIconMenu = false; - private static boolean enablePredictiveBackGesture = true; - private static boolean enablePrivateSpace = true; - private static boolean enablePrivateSpaceInstallShortcut = true; - private static boolean enableRebootUnlockAnimation = false; - private static boolean enableRecentsInTaskbar = false; - private static boolean enableRefactorTaskThumbnail = false; - private static boolean enableResponsiveWorkspace = true; - private static boolean enableScalingRevealHomeAnimation = true; - private static boolean enableShortcutDontSuggestApp = true; - private static boolean enableSmartspaceAsAWidget = false; - private static boolean enableSmartspaceRemovalToggle = false; - private static boolean enableSupportForArchiving = false; - private static boolean enableTabletTwoPanePickerV2 = false; - private static boolean enableTaskbarCustomization = false; - private static boolean enableTaskbarNoRecreate = false; - private static boolean enableTaskbarPinning = true; - private static boolean enableTwoPaneLauncherSettings = false; - private static boolean enableTwolineAllapps = false; - private static boolean enableTwolineToggle = true; - private static boolean enableUnfoldStateAnimation = false; - private static boolean enableUnfoldedTwoPanePicker = true; - private static boolean enableWidgetTapToAdd = true; - private static boolean enableWorkspaceInflation = true; - private static boolean enabledFoldersInAllApps = false; - private static boolean floatingSearchBar = false; - private static boolean forceMonochromeAppIcons = false; - private static boolean privateSpaceAddFloatingMaskView = false; - private static boolean privateSpaceAnimation = true; - private static boolean privateSpaceAppInstallerButton = true; - private static boolean privateSpaceRestrictAccessibilityDrag = true; - private static boolean privateSpaceRestrictItemDrag = true; - private static boolean privateSpaceSysAppsSeparation = true; - private static boolean useActivityOverlay = true; + @Override - private void init() { - isCached = true; - } - - private void load_overrides_launcher() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - enableAddAppWidgetViaConfigActivityV2 = - properties.getBoolean(Flags.FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2, true); - enableAdditionalHomeAnimations = - properties.getBoolean(Flags.FLAG_ENABLE_ADDITIONAL_HOME_ANIMATIONS, true); - enableCategorizedWidgetSuggestions = - properties.getBoolean(Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS, true); - enableCursorHoverStates = - properties.getBoolean(Flags.FLAG_ENABLE_CURSOR_HOVER_STATES, true); - enableExpandingPauseWorkButton = - properties.getBoolean(Flags.FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON, true); - enableFallbackOverviewInWindow = - properties.getBoolean(Flags.FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW, false); - enableFocusOutline = - properties.getBoolean(Flags.FLAG_ENABLE_FOCUS_OUTLINE, true); - enableGeneratedPreviews = - properties.getBoolean(Flags.FLAG_ENABLE_GENERATED_PREVIEWS, true); - enableGridOnlyOverview = - properties.getBoolean(Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, false); - enableHandleDelayedGestureCallbacks = - properties.getBoolean(Flags.FLAG_ENABLE_HANDLE_DELAYED_GESTURE_CALLBACKS, true); - enableHomeTransitionListener = - properties.getBoolean(Flags.FLAG_ENABLE_HOME_TRANSITION_LISTENER, true); - enableOverviewIconMenu = - properties.getBoolean(Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, false); - enablePredictiveBackGesture = - properties.getBoolean(Flags.FLAG_ENABLE_PREDICTIVE_BACK_GESTURE, true); - enablePrivateSpaceInstallShortcut = - properties.getBoolean(Flags.FLAG_ENABLE_PRIVATE_SPACE_INSTALL_SHORTCUT, true); - enableRebootUnlockAnimation = - properties.getBoolean(Flags.FLAG_ENABLE_REBOOT_UNLOCK_ANIMATION, false); - enableRecentsInTaskbar = - properties.getBoolean(Flags.FLAG_ENABLE_RECENTS_IN_TASKBAR, false); - enableRefactorTaskThumbnail = - properties.getBoolean(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, false); - enableResponsiveWorkspace = - properties.getBoolean(Flags.FLAG_ENABLE_RESPONSIVE_WORKSPACE, true); - enableScalingRevealHomeAnimation = - properties.getBoolean(Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION, true); - enableShortcutDontSuggestApp = - properties.getBoolean(Flags.FLAG_ENABLE_SHORTCUT_DONT_SUGGEST_APP, true); - enableSmartspaceAsAWidget = - properties.getBoolean(Flags.FLAG_ENABLE_SMARTSPACE_AS_A_WIDGET, false); - enableSmartspaceRemovalToggle = - properties.getBoolean(Flags.FLAG_ENABLE_SMARTSPACE_REMOVAL_TOGGLE, true); - enableSupportForArchiving = - properties.getBoolean(Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING, BuildCompat.isAtLeastU()); - enableTabletTwoPanePickerV2 = - properties.getBoolean(Flags.FLAG_ENABLE_TABLET_TWO_PANE_PICKER_V2, false); - enableTaskbarCustomization = - properties.getBoolean(Flags.FLAG_ENABLE_TASKBAR_CUSTOMIZATION, false); - enableTaskbarNoRecreate = - properties.getBoolean(Flags.FLAG_ENABLE_TASKBAR_NO_RECREATE, false); - enableTaskbarPinning = - properties.getBoolean(Flags.FLAG_ENABLE_TASKBAR_PINNING, true); - enableTwoPaneLauncherSettings = - properties.getBoolean(Flags.FLAG_ENABLE_TWO_PANE_LAUNCHER_SETTINGS, true); - enableTwolineAllapps = - properties.getBoolean(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS, false); - enableTwolineToggle = - properties.getBoolean(Flags.FLAG_ENABLE_TWOLINE_TOGGLE, true); - enableUnfoldStateAnimation = - properties.getBoolean(Flags.FLAG_ENABLE_UNFOLD_STATE_ANIMATION, false); - enableUnfoldedTwoPanePicker = - properties.getBoolean(Flags.FLAG_ENABLE_UNFOLDED_TWO_PANE_PICKER, true); - enableWidgetTapToAdd = - properties.getBoolean(Flags.FLAG_ENABLE_WIDGET_TAP_TO_ADD, true); - enableWorkspaceInflation = - properties.getBoolean(Flags.FLAG_ENABLE_WORKSPACE_INFLATION, true); - enabledFoldersInAllApps = - properties.getBoolean(Flags.FLAG_ENABLED_FOLDERS_IN_ALL_APPS, false); - floatingSearchBar = - properties.getBoolean(Flags.FLAG_FLOATING_SEARCH_BAR, false); - forceMonochromeAppIcons = - properties.getBoolean(Flags.FLAG_FORCE_MONOCHROME_APP_ICONS, true); - useActivityOverlay = - properties.getBoolean(Flags.FLAG_USE_ACTIVITY_OVERLAY, true); - } catch (NullPointerException e) { - // Ignored - } - launcher_is_cached = true; - } - - private void load_overrides_launcher_search() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - enablePrivateSpace = - properties.getBoolean(Flags.FLAG_ENABLE_PRIVATE_SPACE, BuildCompat.isAtLeastU()); - privateSpaceAddFloatingMaskView = - properties.getBoolean(Flags.FLAG_PRIVATE_SPACE_ADD_FLOATING_MASK_VIEW, BuildCompat.isAtLeastU()); - privateSpaceAnimation = - properties.getBoolean(Flags.FLAG_PRIVATE_SPACE_ANIMATION, BuildCompat.isAtLeastU()); - privateSpaceAppInstallerButton = - properties.getBoolean(Flags.FLAG_PRIVATE_SPACE_APP_INSTALLER_BUTTON, BuildCompat.isAtLeastU()); - privateSpaceRestrictAccessibilityDrag = - properties.getBoolean(Flags.FLAG_PRIVATE_SPACE_RESTRICT_ACCESSIBILITY_DRAG, BuildCompat.isAtLeastU()); - privateSpaceRestrictItemDrag = - properties.getBoolean(Flags.FLAG_PRIVATE_SPACE_RESTRICT_ITEM_DRAG, BuildCompat.isAtLeastU()); - privateSpaceSysAppsSeparation = - properties.getBoolean(Flags.FLAG_PRIVATE_SPACE_SYS_APPS_SEPARATION, BuildCompat.isAtLeastU()); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace launcher_search " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - launcher_search_is_cached = true; + public boolean accessibilityScrollOnAllapps() { + return true; } @Override - public boolean enableAddAppWidgetViaConfigActivityV2() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableAddAppWidgetViaConfigActivityV2; + + public boolean allAppsBlur() { + return true; } + @Override + + + public boolean allAppsSheetForHandheld() { + return true; + } + + @Override + + + public boolean coordinateWorkspaceScale() { + return true; + } + + @Override + + + public boolean enableActiveGestureProtoLog() { + return true; + } + + @Override + + + public boolean enableAddAppWidgetViaConfigActivityV2() { + return true; + } + + @Override + + public boolean enableAdditionalHomeAnimations() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableAdditionalHomeAnimations; - + return true; } @Override + + + public boolean enableAllAppsButtonInHotseat() { + return false; + } + + @Override + + + public boolean enableAltTabKqsFlatenning() { + return false; + } + + @Override + + + public boolean enableAltTabKqsOnConnectedDisplays() { + return false; + } + + @Override + + public boolean enableCategorizedWidgetSuggestions() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableCategorizedWidgetSuggestions; - + return true; } @Override + + + public boolean enableContainerReturnAnimations() { + return true; + } + + @Override + + + public boolean enableContrastTiles() { + return false; + } + + @Override + + public boolean enableCursorHoverStates() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableCursorHoverStates; - + return true; } @Override + + + public boolean enableDesktopExplodedView() { + return false; + } + + @Override + + + public boolean enableDesktopTaskAlphaAnimation() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingCarouselDetach() { + return false; + } + + @Override + + + public boolean enableDismissPredictionUndo() { + return true; + } + + @Override + + public boolean enableExpandingPauseWorkButton() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableExpandingPauseWorkButton; - + return true; } @Override + + + public boolean enableExpressiveDismissTaskMotion() { + return true; + } + + @Override + + public boolean enableFallbackOverviewInWindow() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableFallbackOverviewInWindow; - + return false; } @Override + + public boolean enableFirstScreenBroadcastArchivingExtras() { - return false; - + return true; } @Override + + public boolean enableFocusOutline() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableFocusOutline; - + return true; } @Override + + public boolean enableGeneratedPreviews() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableGeneratedPreviews; - + return true; } @Override - public boolean enableGridMigrationFix() { + + + public boolean enableGestureNavHorizontalTouchSlop() { return false; - } @Override + + + public boolean enableGestureNavOnConnectedDisplays() { + return false; + } + + @Override + + + public boolean enableGridMigrationFix() { + return true; + } + + @Override + + public boolean enableGridOnlyOverview() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableGridOnlyOverview; - + return false; } @Override + + + public boolean enableGrowthNudge() { + return false; + } + + @Override + + public boolean enableHandleDelayedGestureCallbacks() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableHandleDelayedGestureCallbacks; - + return true; } @Override + + public boolean enableHomeTransitionListener() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableHomeTransitionListener; - + return true; } @Override + + + public boolean enableHoverOfChildElementsInTaskview() { + return true; + } + + @Override + + + public boolean enableLargeDesktopWindowingTile() { + return true; + } + + @Override + + public boolean enableLauncherBrMetricsFixed() { return true; - } @Override - public boolean enableNarrowGridRestore() { + + + public boolean enableLauncherIconShapes() { + return true; + } + + @Override + + + public boolean enableLauncherOverviewInWindow() { return false; - } @Override + + + public boolean enableLauncherVisualRefresh() { + return true; + } + + @Override + + + public boolean enableMouseInteractionChanges() { + return true; + } + + @Override + + + public boolean enableMultiInstanceMenuTaskbar() { + return true; + } + + @Override + + + public boolean enableNarrowGridRestore() { + return true; + } + + @Override + + + public boolean enableOverviewBackgroundWallpaperBlur() { + return true; + } + + @Override + + + public boolean enableOverviewCommandHelperTimeout() { + return true; + } + + @Override + + + public boolean enableOverviewDesktopTileWallpaperBackground() { + return false; + } + + @Override + + public boolean enableOverviewIconMenu() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableOverviewIconMenu; - + return false; } @Override + + + public boolean enableOverviewOnConnectedDisplays() { + return false; + } + + @Override + + + public boolean enablePinningAppWithContextMenu() { + return false; + } + + @Override + + public boolean enablePredictiveBackGesture() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enablePredictiveBackGesture; - + return true; } @Override + + public boolean enablePrivateSpace() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_search_is_cached) { - load_overrides_launcher_search(); - } - } - return enablePrivateSpace; - + return true; } @Override + + public boolean enablePrivateSpaceInstallShortcut() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enablePrivateSpaceInstallShortcut; - + return true; } @Override + + public boolean enableRebootUnlockAnimation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableRebootUnlockAnimation; - + return false; } @Override + + public boolean enableRecentsInTaskbar() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableRecentsInTaskbar; - + return false; } @Override + + + public boolean enableRecentsWindowProtoLog() { + return false; + } + + @Override + + public boolean enableRefactorTaskThumbnail() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableRefactorTaskThumbnail; - + return false; } @Override + + public boolean enableResponsiveWorkspace() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableResponsiveWorkspace; - + return true; } @Override + + + public boolean enableScalabilityForDesktopExperience() { + return false; + } + + @Override + + public boolean enableScalingRevealHomeAnimation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableScalingRevealHomeAnimation; - + return true; } @Override + + + public boolean enableSeparateExternalDisplayTasks() { + return true; + } + + @Override + + public boolean enableShortcutDontSuggestApp() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableShortcutDontSuggestApp; - + return true; } @Override + + + public boolean enableShowEnabledShortcutsInAccessibilityMenu() { + return true; + } + + @Override + + public boolean enableSmartspaceAsAWidget() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableSmartspaceAsAWidget; - + return false; } @Override + + public boolean enableSmartspaceRemovalToggle() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableSmartspaceRemovalToggle; - + return false; } @Override + + + public boolean enableStateManagerProtoLog() { + return false; + } + + @Override + + + public boolean enableStrictMode() { + return false; + } + + @Override + + public boolean enableSupportForArchiving() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableSupportForArchiving; - + return true; } @Override + + public boolean enableTabletTwoPanePickerV2() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableTabletTwoPanePickerV2; - + return false; } @Override + + + public boolean enableTaskbarBehindShade() { + return false; + } + + @Override + + public boolean enableTaskbarCustomization() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableTaskbarCustomization; - + return false; } @Override + + + public boolean enableTaskbarForDirectBoot() { + return false; + } + + @Override + + public boolean enableTaskbarNoRecreate() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableTaskbarNoRecreate; - + return false; } @Override + + public boolean enableTaskbarPinning() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableTaskbarPinning; - + return true; } @Override + + + public boolean enableTieredWidgetsByDefaultInPicker() { + return false; + } + + @Override + + public boolean enableTwoPaneLauncherSettings() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableTwoPaneLauncherSettings; - + return true; } @Override + + public boolean enableTwolineAllapps() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableTwolineAllapps; - + return false; } @Override + + public boolean enableTwolineToggle() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableTwolineToggle; - + return true; } @Override + + public boolean enableUnfoldStateAnimation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableUnfoldStateAnimation; - + return true; } @Override + + public boolean enableUnfoldedTwoPanePicker() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableUnfoldedTwoPanePicker; - + return true; } @Override + + + public boolean enableUseTopVisibleActivityForExcludeFromRecentTask() { + return true; + } + + @Override + + public boolean enableWidgetTapToAdd() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableWidgetTapToAdd; - + return true; } @Override + + public boolean enableWorkspaceInflation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enableWorkspaceInflation; - + return true; } @Override + + public boolean enabledFoldersInAllApps() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return enabledFoldersInAllApps; - + return false; } @Override + + + public boolean expressiveThemeInTaskbarAndNavigation() { + return true; + } + + @Override + + + public boolean extendibleThemeManager() { + return true; + } + + @Override + + public boolean floatingSearchBar() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return floatingSearchBar; - + return false; } @Override + + public boolean forceMonochromeAppIcons() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return forceMonochromeAppIcons; - + return false; } @Override + + + public boolean gridMigrationRefactor() { + return true; + } + + @Override + + + public boolean gsfRes() { + return false; + } + + @Override + + + public boolean ignoreThreeFingerTrackpadForNavHandleLongPress() { + return true; + } + + @Override + + + public boolean letterFastScroller() { + return false; + } + + @Override + + + public boolean msdlFeedback() { + return true; + } + + @Override + + + public boolean multilineSearchBar() { + return true; + } + + @Override + + + public boolean navigateToChildPreference() { + return true; + } + + @Override + + + public boolean oneGridMountedMode() { + return false; + } + + @Override + + + public boolean oneGridRotationHandling() { + return false; + } + + @Override + + + public boolean oneGridSpecs() { + return false; + } + + @Override + + + public boolean predictiveBackToHomeBlur() { + return true; + } + + @Override + + + public boolean predictiveBackToHomePolish() { + return true; + } + + @Override + + public boolean privateSpaceAddFloatingMaskView() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_search_is_cached) { - load_overrides_launcher_search(); - } - } - return privateSpaceAddFloatingMaskView; - + return true; } @Override + + public boolean privateSpaceAnimation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_search_is_cached) { - load_overrides_launcher_search(); - } - } - return privateSpaceAnimation; - + return true; } @Override + + public boolean privateSpaceAppInstallerButton() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_search_is_cached) { - load_overrides_launcher_search(); - } - } - return privateSpaceAppInstallerButton; - + return true; } @Override + + public boolean privateSpaceRestrictAccessibilityDrag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_search_is_cached) { - load_overrides_launcher_search(); - } - } - return privateSpaceRestrictAccessibilityDrag; - + return true; } @Override + + public boolean privateSpaceRestrictItemDrag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_search_is_cached) { - load_overrides_launcher_search(); - } - } - return privateSpaceRestrictItemDrag; - + return true; } @Override + + public boolean privateSpaceSysAppsSeparation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_search_is_cached) { - load_overrides_launcher_search(); - } - } - return privateSpaceSysAppsSeparation; - + return true; } @Override - public boolean useActivityOverlay() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!launcher_is_cached) { - load_overrides_launcher(); - } - } - return useActivityOverlay; + + public boolean removeAppsRefreshOnRightClick() { + return true; + } + + @Override + + + public boolean removeExcludeFromScreenMagnificationFlagUsage() { + return true; + } + + @Override + + + public boolean restoreArchivedAppIconsFromDb() { + return false; + } + + @Override + + + public boolean restoreArchivedShortcuts() { + return false; + } + + @Override + + + public boolean showTaskbarPinningPopupFromAnywhere() { + return false; + } + + @Override + + + public boolean syncAppLaunchWithTaskbarStash() { + return false; + } + + @Override + + + public boolean taskbarOverflow() { + return true; + } + + @Override + + + public boolean taskbarQuietModeChangeSupport() { + return false; + } + + @Override + + + public boolean useActivityOverlay() { + return true; + } + + @Override + + + public boolean useNewIconForArchivedApps() { + return true; + } + + @Override + + + public boolean useSystemRadiusForAppWidgets() { + return true; + } + + @Override + + + public boolean workSchedulerInWorkProfile() { + return false; } } - diff --git a/flags/src/com/android/launcher3/Flags.java b/flags/src/com/android/launcher3/Flags.java index 3e37a8f2a8..68ce0509a9 100644 --- a/flags/src/com/android/launcher3/Flags.java +++ b/flags/src/com/android/launcher3/Flags.java @@ -1,18 +1,49 @@ package com.android.launcher3; // TODO(b/303773055): Remove the annotation after access issue is resolved. + /** @hide */ public final class Flags { + /** @hide */ + public static final String FLAG_ACCESSIBILITY_SCROLL_ON_ALLAPPS = "com.android.launcher3.accessibility_scroll_on_allapps"; + /** @hide */ + public static final String FLAG_ALL_APPS_BLUR = "com.android.launcher3.all_apps_blur"; + /** @hide */ + public static final String FLAG_ALL_APPS_SHEET_FOR_HANDHELD = "com.android.launcher3.all_apps_sheet_for_handheld"; + /** @hide */ + public static final String FLAG_COORDINATE_WORKSPACE_SCALE = "com.android.launcher3.coordinate_workspace_scale"; + /** @hide */ + public static final String FLAG_ENABLE_ACTIVE_GESTURE_PROTO_LOG = "com.android.launcher3.enable_active_gesture_proto_log"; /** @hide */ public static final String FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 = "com.android.launcher3.enable_add_app_widget_via_config_activity_v2"; /** @hide */ public static final String FLAG_ENABLE_ADDITIONAL_HOME_ANIMATIONS = "com.android.launcher3.enable_additional_home_animations"; /** @hide */ + public static final String FLAG_ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT = "com.android.launcher3.enable_all_apps_button_in_hotseat"; + /** @hide */ + public static final String FLAG_ENABLE_ALT_TAB_KQS_FLATENNING = "com.android.launcher3.enable_alt_tab_kqs_flatenning"; + /** @hide */ + public static final String FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS = "com.android.launcher3.enable_alt_tab_kqs_on_connected_displays"; + /** @hide */ public static final String FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS = "com.android.launcher3.enable_categorized_widget_suggestions"; /** @hide */ + public static final String FLAG_ENABLE_CONTAINER_RETURN_ANIMATIONS = "com.android.launcher3.enable_container_return_animations"; + /** @hide */ + public static final String FLAG_ENABLE_CONTRAST_TILES = "com.android.launcher3.enable_contrast_tiles"; + /** @hide */ public static final String FLAG_ENABLE_CURSOR_HOVER_STATES = "com.android.launcher3.enable_cursor_hover_states"; /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_EXPLODED_VIEW = "com.android.launcher3.enable_desktop_exploded_view"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_TASK_ALPHA_ANIMATION = "com.android.launcher3.enable_desktop_task_alpha_animation"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_CAROUSEL_DETACH = "com.android.launcher3.enable_desktop_windowing_carousel_detach"; + /** @hide */ + public static final String FLAG_ENABLE_DISMISS_PREDICTION_UNDO = "com.android.launcher3.enable_dismiss_prediction_undo"; + /** @hide */ public static final String FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON = "com.android.launcher3.enable_expanding_pause_work_button"; /** @hide */ + public static final String FLAG_ENABLE_EXPRESSIVE_DISMISS_TASK_MOTION = "com.android.launcher3.enable_expressive_dismiss_task_motion"; + /** @hide */ public static final String FLAG_ENABLE_FALLBACK_OVERVIEW_IN_WINDOW = "com.android.launcher3.enable_fallback_overview_in_window"; /** @hide */ public static final String FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS = "com.android.launcher3.enable_first_screen_broadcast_archiving_extras"; @@ -21,20 +52,50 @@ public final class Flags { /** @hide */ public static final String FLAG_ENABLE_GENERATED_PREVIEWS = "com.android.launcher3.enable_generated_previews"; /** @hide */ + public static final String FLAG_ENABLE_GESTURE_NAV_HORIZONTAL_TOUCH_SLOP = "com.android.launcher3.enable_gesture_nav_horizontal_touch_slop"; + /** @hide */ + public static final String FLAG_ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS = "com.android.launcher3.enable_gesture_nav_on_connected_displays"; + /** @hide */ public static final String FLAG_ENABLE_GRID_MIGRATION_FIX = "com.android.launcher3.enable_grid_migration_fix"; /** @hide */ public static final String FLAG_ENABLE_GRID_ONLY_OVERVIEW = "com.android.launcher3.enable_grid_only_overview"; /** @hide */ + public static final String FLAG_ENABLE_GROWTH_NUDGE = "com.android.launcher3.enable_growth_nudge"; + /** @hide */ public static final String FLAG_ENABLE_HANDLE_DELAYED_GESTURE_CALLBACKS = "com.android.launcher3.enable_handle_delayed_gesture_callbacks"; /** @hide */ public static final String FLAG_ENABLE_HOME_TRANSITION_LISTENER = "com.android.launcher3.enable_home_transition_listener"; /** @hide */ + public static final String FLAG_ENABLE_HOVER_OF_CHILD_ELEMENTS_IN_TASKVIEW = "com.android.launcher3.enable_hover_of_child_elements_in_taskview"; + /** @hide */ + public static final String FLAG_ENABLE_LARGE_DESKTOP_WINDOWING_TILE = "com.android.launcher3.enable_large_desktop_windowing_tile"; + /** @hide */ public static final String FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED = "com.android.launcher3.enable_launcher_br_metrics_fixed"; /** @hide */ + public static final String FLAG_ENABLE_LAUNCHER_ICON_SHAPES = "com.android.launcher3.enable_launcher_icon_shapes"; + /** @hide */ + public static final String FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW = "com.android.launcher3.enable_launcher_overview_in_window"; + /** @hide */ + public static final String FLAG_ENABLE_LAUNCHER_VISUAL_REFRESH = "com.android.launcher3.enable_launcher_visual_refresh"; + /** @hide */ + public static final String FLAG_ENABLE_MOUSE_INTERACTION_CHANGES = "com.android.launcher3.enable_mouse_interaction_changes"; + /** @hide */ + public static final String FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR = "com.android.launcher3.enable_multi_instance_menu_taskbar"; + /** @hide */ public static final String FLAG_ENABLE_NARROW_GRID_RESTORE = "com.android.launcher3.enable_narrow_grid_restore"; /** @hide */ + public static final String FLAG_ENABLE_OVERVIEW_BACKGROUND_WALLPAPER_BLUR = "com.android.launcher3.enable_overview_background_wallpaper_blur"; + /** @hide */ + public static final String FLAG_ENABLE_OVERVIEW_COMMAND_HELPER_TIMEOUT = "com.android.launcher3.enable_overview_command_helper_timeout"; + /** @hide */ + public static final String FLAG_ENABLE_OVERVIEW_DESKTOP_TILE_WALLPAPER_BACKGROUND = "com.android.launcher3.enable_overview_desktop_tile_wallpaper_background"; + /** @hide */ public static final String FLAG_ENABLE_OVERVIEW_ICON_MENU = "com.android.launcher3.enable_overview_icon_menu"; /** @hide */ + public static final String FLAG_ENABLE_OVERVIEW_ON_CONNECTED_DISPLAYS = "com.android.launcher3.enable_overview_on_connected_displays"; + /** @hide */ + public static final String FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU = "com.android.launcher3.enable_pinning_app_with_context_menu"; + /** @hide */ public static final String FLAG_ENABLE_PREDICTIVE_BACK_GESTURE = "com.android.launcher3.enable_predictive_back_gesture"; /** @hide */ public static final String FLAG_ENABLE_PRIVATE_SPACE = "com.android.launcher3.enable_private_space"; @@ -45,28 +106,46 @@ public final class Flags { /** @hide */ public static final String FLAG_ENABLE_RECENTS_IN_TASKBAR = "com.android.launcher3.enable_recents_in_taskbar"; /** @hide */ + public static final String FLAG_ENABLE_RECENTS_WINDOW_PROTO_LOG = "com.android.launcher3.enable_recents_window_proto_log"; + /** @hide */ public static final String FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL = "com.android.launcher3.enable_refactor_task_thumbnail"; /** @hide */ public static final String FLAG_ENABLE_RESPONSIVE_WORKSPACE = "com.android.launcher3.enable_responsive_workspace"; /** @hide */ + public static final String FLAG_ENABLE_SCALABILITY_FOR_DESKTOP_EXPERIENCE = "com.android.launcher3.enable_scalability_for_desktop_experience"; + /** @hide */ public static final String FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION = "com.android.launcher3.enable_scaling_reveal_home_animation"; /** @hide */ + public static final String FLAG_ENABLE_SEPARATE_EXTERNAL_DISPLAY_TASKS = "com.android.launcher3.enable_separate_external_display_tasks"; + /** @hide */ public static final String FLAG_ENABLE_SHORTCUT_DONT_SUGGEST_APP = "com.android.launcher3.enable_shortcut_dont_suggest_app"; /** @hide */ + public static final String FLAG_ENABLE_SHOW_ENABLED_SHORTCUTS_IN_ACCESSIBILITY_MENU = "com.android.launcher3.enable_show_enabled_shortcuts_in_accessibility_menu"; + /** @hide */ public static final String FLAG_ENABLE_SMARTSPACE_AS_A_WIDGET = "com.android.launcher3.enable_smartspace_as_a_widget"; /** @hide */ public static final String FLAG_ENABLE_SMARTSPACE_REMOVAL_TOGGLE = "com.android.launcher3.enable_smartspace_removal_toggle"; /** @hide */ + public static final String FLAG_ENABLE_STATE_MANAGER_PROTO_LOG = "com.android.launcher3.enable_state_manager_proto_log"; + /** @hide */ + public static final String FLAG_ENABLE_STRICT_MODE = "com.android.launcher3.enable_strict_mode"; + /** @hide */ public static final String FLAG_ENABLE_SUPPORT_FOR_ARCHIVING = "com.android.launcher3.enable_support_for_archiving"; /** @hide */ public static final String FLAG_ENABLE_TABLET_TWO_PANE_PICKER_V2 = "com.android.launcher3.enable_tablet_two_pane_picker_v2"; /** @hide */ + public static final String FLAG_ENABLE_TASKBAR_BEHIND_SHADE = "com.android.launcher3.enable_taskbar_behind_shade"; + /** @hide */ public static final String FLAG_ENABLE_TASKBAR_CUSTOMIZATION = "com.android.launcher3.enable_taskbar_customization"; /** @hide */ + public static final String FLAG_ENABLE_TASKBAR_FOR_DIRECT_BOOT = "com.android.launcher3.enable_taskbar_for_direct_boot"; + /** @hide */ public static final String FLAG_ENABLE_TASKBAR_NO_RECREATE = "com.android.launcher3.enable_taskbar_no_recreate"; /** @hide */ public static final String FLAG_ENABLE_TASKBAR_PINNING = "com.android.launcher3.enable_taskbar_pinning"; /** @hide */ + public static final String FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER = "com.android.launcher3.enable_tiered_widgets_by_default_in_picker"; + /** @hide */ public static final String FLAG_ENABLE_TWO_PANE_LAUNCHER_SETTINGS = "com.android.launcher3.enable_two_pane_launcher_settings"; /** @hide */ public static final String FLAG_ENABLE_TWOLINE_ALLAPPS = "com.android.launcher3.enable_twoline_allapps"; @@ -77,16 +156,46 @@ public final class Flags { /** @hide */ public static final String FLAG_ENABLE_UNFOLDED_TWO_PANE_PICKER = "com.android.launcher3.enable_unfolded_two_pane_picker"; /** @hide */ + public static final String FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK = "com.android.launcher3.enable_use_top_visible_activity_for_exclude_from_recent_task"; + /** @hide */ public static final String FLAG_ENABLE_WIDGET_TAP_TO_ADD = "com.android.launcher3.enable_widget_tap_to_add"; /** @hide */ public static final String FLAG_ENABLE_WORKSPACE_INFLATION = "com.android.launcher3.enable_workspace_inflation"; /** @hide */ public static final String FLAG_ENABLED_FOLDERS_IN_ALL_APPS = "com.android.launcher3.enabled_folders_in_all_apps"; /** @hide */ + public static final String FLAG_EXPRESSIVE_THEME_IN_TASKBAR_AND_NAVIGATION = "com.android.launcher3.expressive_theme_in_taskbar_and_navigation"; + /** @hide */ + public static final String FLAG_EXTENDIBLE_THEME_MANAGER = "com.android.launcher3.extendible_theme_manager"; + /** @hide */ public static final String FLAG_FLOATING_SEARCH_BAR = "com.android.launcher3.floating_search_bar"; /** @hide */ public static final String FLAG_FORCE_MONOCHROME_APP_ICONS = "com.android.launcher3.force_monochrome_app_icons"; /** @hide */ + public static final String FLAG_GRID_MIGRATION_REFACTOR = "com.android.launcher3.grid_migration_refactor"; + /** @hide */ + public static final String FLAG_GSF_RES = "com.android.launcher3.gsf_res"; + /** @hide */ + public static final String FLAG_IGNORE_THREE_FINGER_TRACKPAD_FOR_NAV_HANDLE_LONG_PRESS = "com.android.launcher3.ignore_three_finger_trackpad_for_nav_handle_long_press"; + /** @hide */ + public static final String FLAG_LETTER_FAST_SCROLLER = "com.android.launcher3.letter_fast_scroller"; + /** @hide */ + public static final String FLAG_MSDL_FEEDBACK = "com.android.launcher3.msdl_feedback"; + /** @hide */ + public static final String FLAG_MULTILINE_SEARCH_BAR = "com.android.launcher3.multiline_search_bar"; + /** @hide */ + public static final String FLAG_NAVIGATE_TO_CHILD_PREFERENCE = "com.android.launcher3.navigate_to_child_preference"; + /** @hide */ + public static final String FLAG_ONE_GRID_MOUNTED_MODE = "com.android.launcher3.one_grid_mounted_mode"; + /** @hide */ + public static final String FLAG_ONE_GRID_ROTATION_HANDLING = "com.android.launcher3.one_grid_rotation_handling"; + /** @hide */ + public static final String FLAG_ONE_GRID_SPECS = "com.android.launcher3.one_grid_specs"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_TO_HOME_BLUR = "com.android.launcher3.predictive_back_to_home_blur"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_TO_HOME_POLISH = "com.android.launcher3.predictive_back_to_home_polish"; + /** @hide */ public static final String FLAG_PRIVATE_SPACE_ADD_FLOATING_MASK_VIEW = "com.android.launcher3.private_space_add_floating_mask_view"; /** @hide */ public static final String FLAG_PRIVATE_SPACE_ANIMATION = "com.android.launcher3.private_space_animation"; @@ -99,204 +208,714 @@ public final class Flags { /** @hide */ public static final String FLAG_PRIVATE_SPACE_SYS_APPS_SEPARATION = "com.android.launcher3.private_space_sys_apps_separation"; /** @hide */ + public static final String FLAG_REMOVE_APPS_REFRESH_ON_RIGHT_CLICK = "com.android.launcher3.remove_apps_refresh_on_right_click"; + /** @hide */ + public static final String FLAG_REMOVE_EXCLUDE_FROM_SCREEN_MAGNIFICATION_FLAG_USAGE = "com.android.launcher3.remove_exclude_from_screen_magnification_flag_usage"; + /** @hide */ + public static final String FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB = "com.android.launcher3.restore_archived_app_icons_from_db"; + /** @hide */ + public static final String FLAG_RESTORE_ARCHIVED_SHORTCUTS = "com.android.launcher3.restore_archived_shortcuts"; + /** @hide */ + public static final String FLAG_SHOW_TASKBAR_PINNING_POPUP_FROM_ANYWHERE = "com.android.launcher3.show_taskbar_pinning_popup_from_anywhere"; + /** @hide */ + public static final String FLAG_SYNC_APP_LAUNCH_WITH_TASKBAR_STASH = "com.android.launcher3.sync_app_launch_with_taskbar_stash"; + /** @hide */ + public static final String FLAG_TASKBAR_OVERFLOW = "com.android.launcher3.taskbar_overflow"; + /** @hide */ + public static final String FLAG_TASKBAR_QUIET_MODE_CHANGE_SUPPORT = "com.android.launcher3.taskbar_quiet_mode_change_support"; + /** @hide */ public static final String FLAG_USE_ACTIVITY_OVERLAY = "com.android.launcher3.use_activity_overlay"; + /** @hide */ + public static final String FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS = "com.android.launcher3.use_new_icon_for_archived_apps"; + /** @hide */ + public static final String FLAG_USE_SYSTEM_RADIUS_FOR_APP_WIDGETS = "com.android.launcher3.use_system_radius_for_app_widgets"; + /** @hide */ + public static final String FLAG_WORK_SCHEDULER_IN_WORK_PROFILE = "com.android.launcher3.work_scheduler_in_work_profile"; + + + public static boolean accessibilityScrollOnAllapps() { + + return FEATURE_FLAGS.accessibilityScrollOnAllapps(); + } + + + public static boolean allAppsBlur() { + + return FEATURE_FLAGS.allAppsBlur(); + } + + + public static boolean allAppsSheetForHandheld() { + + return FEATURE_FLAGS.allAppsSheetForHandheld(); + } + + + public static boolean coordinateWorkspaceScale() { + + return FEATURE_FLAGS.coordinateWorkspaceScale(); + } + + + public static boolean enableActiveGestureProtoLog() { + + return FEATURE_FLAGS.enableActiveGestureProtoLog(); + } + public static boolean enableAddAppWidgetViaConfigActivityV2() { + return FEATURE_FLAGS.enableAddAppWidgetViaConfigActivityV2(); } + public static boolean enableAdditionalHomeAnimations() { + return FEATURE_FLAGS.enableAdditionalHomeAnimations(); } + + public static boolean enableAllAppsButtonInHotseat() { + + return FEATURE_FLAGS.enableAllAppsButtonInHotseat(); + } + + + public static boolean enableAltTabKqsFlatenning() { + + return FEATURE_FLAGS.enableAltTabKqsFlatenning(); + } + + + public static boolean enableAltTabKqsOnConnectedDisplays() { + + return FEATURE_FLAGS.enableAltTabKqsOnConnectedDisplays(); + } + + public static boolean enableCategorizedWidgetSuggestions() { + return FEATURE_FLAGS.enableCategorizedWidgetSuggestions(); } + + public static boolean enableContainerReturnAnimations() { + + return FEATURE_FLAGS.enableContainerReturnAnimations(); + } + + + public static boolean enableContrastTiles() { + + return FEATURE_FLAGS.enableContrastTiles(); + } + + public static boolean enableCursorHoverStates() { + return FEATURE_FLAGS.enableCursorHoverStates(); } + + public static boolean enableDesktopExplodedView() { + + return FEATURE_FLAGS.enableDesktopExplodedView(); + } + + + public static boolean enableDesktopTaskAlphaAnimation() { + + return FEATURE_FLAGS.enableDesktopTaskAlphaAnimation(); + } + + + public static boolean enableDesktopWindowingCarouselDetach() { + + return FEATURE_FLAGS.enableDesktopWindowingCarouselDetach(); + } + + + public static boolean enableDismissPredictionUndo() { + + return FEATURE_FLAGS.enableDismissPredictionUndo(); + } + + public static boolean enableExpandingPauseWorkButton() { + return FEATURE_FLAGS.enableExpandingPauseWorkButton(); } + + public static boolean enableExpressiveDismissTaskMotion() { + + return FEATURE_FLAGS.enableExpressiveDismissTaskMotion(); + } + + public static boolean enableFallbackOverviewInWindow() { + return FEATURE_FLAGS.enableFallbackOverviewInWindow(); } + public static boolean enableFirstScreenBroadcastArchivingExtras() { + return FEATURE_FLAGS.enableFirstScreenBroadcastArchivingExtras(); } + public static boolean enableFocusOutline() { + return FEATURE_FLAGS.enableFocusOutline(); } + public static boolean enableGeneratedPreviews() { + return FEATURE_FLAGS.enableGeneratedPreviews(); } + + public static boolean enableGestureNavHorizontalTouchSlop() { + + return FEATURE_FLAGS.enableGestureNavHorizontalTouchSlop(); + } + + + public static boolean enableGestureNavOnConnectedDisplays() { + + return FEATURE_FLAGS.enableGestureNavOnConnectedDisplays(); + } + + public static boolean enableGridMigrationFix() { + return FEATURE_FLAGS.enableGridMigrationFix(); } + public static boolean enableGridOnlyOverview() { + return FEATURE_FLAGS.enableGridOnlyOverview(); } + + public static boolean enableGrowthNudge() { + + return FEATURE_FLAGS.enableGrowthNudge(); + } + + public static boolean enableHandleDelayedGestureCallbacks() { + return FEATURE_FLAGS.enableHandleDelayedGestureCallbacks(); } + public static boolean enableHomeTransitionListener() { + return FEATURE_FLAGS.enableHomeTransitionListener(); } + + public static boolean enableHoverOfChildElementsInTaskview() { + + return FEATURE_FLAGS.enableHoverOfChildElementsInTaskview(); + } + + + public static boolean enableLargeDesktopWindowingTile() { + + return FEATURE_FLAGS.enableLargeDesktopWindowingTile(); + } + + public static boolean enableLauncherBrMetricsFixed() { + return FEATURE_FLAGS.enableLauncherBrMetricsFixed(); } + + public static boolean enableLauncherIconShapes() { + + return FEATURE_FLAGS.enableLauncherIconShapes(); + } + + + public static boolean enableLauncherOverviewInWindow() { + + return FEATURE_FLAGS.enableLauncherOverviewInWindow(); + } + + + public static boolean enableLauncherVisualRefresh() { + + return FEATURE_FLAGS.enableLauncherVisualRefresh(); + } + + + public static boolean enableMouseInteractionChanges() { + + return FEATURE_FLAGS.enableMouseInteractionChanges(); + } + + + public static boolean enableMultiInstanceMenuTaskbar() { + + return FEATURE_FLAGS.enableMultiInstanceMenuTaskbar(); + } + + public static boolean enableNarrowGridRestore() { + return FEATURE_FLAGS.enableNarrowGridRestore(); } + + public static boolean enableOverviewBackgroundWallpaperBlur() { + + return FEATURE_FLAGS.enableOverviewBackgroundWallpaperBlur(); + } + + + public static boolean enableOverviewCommandHelperTimeout() { + + return FEATURE_FLAGS.enableOverviewCommandHelperTimeout(); + } + + + public static boolean enableOverviewDesktopTileWallpaperBackground() { + + return FEATURE_FLAGS.enableOverviewDesktopTileWallpaperBackground(); + } + + public static boolean enableOverviewIconMenu() { + return FEATURE_FLAGS.enableOverviewIconMenu(); } + + public static boolean enableOverviewOnConnectedDisplays() { + + return FEATURE_FLAGS.enableOverviewOnConnectedDisplays(); + } + + + public static boolean enablePinningAppWithContextMenu() { + + return FEATURE_FLAGS.enablePinningAppWithContextMenu(); + } + + public static boolean enablePredictiveBackGesture() { + return FEATURE_FLAGS.enablePredictiveBackGesture(); } + public static boolean enablePrivateSpace() { + return FEATURE_FLAGS.enablePrivateSpace(); } + public static boolean enablePrivateSpaceInstallShortcut() { + return FEATURE_FLAGS.enablePrivateSpaceInstallShortcut(); } + public static boolean enableRebootUnlockAnimation() { + return FEATURE_FLAGS.enableRebootUnlockAnimation(); } + public static boolean enableRecentsInTaskbar() { + return FEATURE_FLAGS.enableRecentsInTaskbar(); } + + public static boolean enableRecentsWindowProtoLog() { + + return FEATURE_FLAGS.enableRecentsWindowProtoLog(); + } + + public static boolean enableRefactorTaskThumbnail() { + return FEATURE_FLAGS.enableRefactorTaskThumbnail(); } + public static boolean enableResponsiveWorkspace() { + return FEATURE_FLAGS.enableResponsiveWorkspace(); } + + public static boolean enableScalabilityForDesktopExperience() { + + return FEATURE_FLAGS.enableScalabilityForDesktopExperience(); + } + + public static boolean enableScalingRevealHomeAnimation() { + return FEATURE_FLAGS.enableScalingRevealHomeAnimation(); } + + public static boolean enableSeparateExternalDisplayTasks() { + + return FEATURE_FLAGS.enableSeparateExternalDisplayTasks(); + } + + public static boolean enableShortcutDontSuggestApp() { + return FEATURE_FLAGS.enableShortcutDontSuggestApp(); } + + public static boolean enableShowEnabledShortcutsInAccessibilityMenu() { + + return FEATURE_FLAGS.enableShowEnabledShortcutsInAccessibilityMenu(); + } + + public static boolean enableSmartspaceAsAWidget() { + return FEATURE_FLAGS.enableSmartspaceAsAWidget(); } + public static boolean enableSmartspaceRemovalToggle() { + return FEATURE_FLAGS.enableSmartspaceRemovalToggle(); } + + public static boolean enableStateManagerProtoLog() { + + return FEATURE_FLAGS.enableStateManagerProtoLog(); + } + + + public static boolean enableStrictMode() { + + return FEATURE_FLAGS.enableStrictMode(); + } + + public static boolean enableSupportForArchiving() { + return FEATURE_FLAGS.enableSupportForArchiving(); } + public static boolean enableTabletTwoPanePickerV2() { + return FEATURE_FLAGS.enableTabletTwoPanePickerV2(); } + + public static boolean enableTaskbarBehindShade() { + + return FEATURE_FLAGS.enableTaskbarBehindShade(); + } + + public static boolean enableTaskbarCustomization() { + return FEATURE_FLAGS.enableTaskbarCustomization(); } + + public static boolean enableTaskbarForDirectBoot() { + + return FEATURE_FLAGS.enableTaskbarForDirectBoot(); + } + + public static boolean enableTaskbarNoRecreate() { + return FEATURE_FLAGS.enableTaskbarNoRecreate(); } + public static boolean enableTaskbarPinning() { + return FEATURE_FLAGS.enableTaskbarPinning(); } + + public static boolean enableTieredWidgetsByDefaultInPicker() { + + return FEATURE_FLAGS.enableTieredWidgetsByDefaultInPicker(); + } + + public static boolean enableTwoPaneLauncherSettings() { + return FEATURE_FLAGS.enableTwoPaneLauncherSettings(); } + public static boolean enableTwolineAllapps() { + return FEATURE_FLAGS.enableTwolineAllapps(); } + public static boolean enableTwolineToggle() { + return FEATURE_FLAGS.enableTwolineToggle(); } + public static boolean enableUnfoldStateAnimation() { + return FEATURE_FLAGS.enableUnfoldStateAnimation(); } + public static boolean enableUnfoldedTwoPanePicker() { + return FEATURE_FLAGS.enableUnfoldedTwoPanePicker(); } + + public static boolean enableUseTopVisibleActivityForExcludeFromRecentTask() { + + return FEATURE_FLAGS.enableUseTopVisibleActivityForExcludeFromRecentTask(); + } + + public static boolean enableWidgetTapToAdd() { + return FEATURE_FLAGS.enableWidgetTapToAdd(); } + public static boolean enableWorkspaceInflation() { + return FEATURE_FLAGS.enableWorkspaceInflation(); } + public static boolean enabledFoldersInAllApps() { + return FEATURE_FLAGS.enabledFoldersInAllApps(); } + + public static boolean expressiveThemeInTaskbarAndNavigation() { + + return FEATURE_FLAGS.expressiveThemeInTaskbarAndNavigation(); + } + + + public static boolean extendibleThemeManager() { + + return FEATURE_FLAGS.extendibleThemeManager(); + } + + public static boolean floatingSearchBar() { + return FEATURE_FLAGS.floatingSearchBar(); } + public static boolean forceMonochromeAppIcons() { + return FEATURE_FLAGS.forceMonochromeAppIcons(); } + + public static boolean gridMigrationRefactor() { + + return FEATURE_FLAGS.gridMigrationRefactor(); + } + + + public static boolean gsfRes() { + + return FEATURE_FLAGS.gsfRes(); + } + + + public static boolean ignoreThreeFingerTrackpadForNavHandleLongPress() { + + return FEATURE_FLAGS.ignoreThreeFingerTrackpadForNavHandleLongPress(); + } + + + public static boolean letterFastScroller() { + + return FEATURE_FLAGS.letterFastScroller(); + } + + + public static boolean msdlFeedback() { + + return FEATURE_FLAGS.msdlFeedback(); + } + + + public static boolean multilineSearchBar() { + + return FEATURE_FLAGS.multilineSearchBar(); + } + + + public static boolean navigateToChildPreference() { + + return FEATURE_FLAGS.navigateToChildPreference(); + } + + + public static boolean oneGridMountedMode() { + + return FEATURE_FLAGS.oneGridMountedMode(); + } + + + public static boolean oneGridRotationHandling() { + + return FEATURE_FLAGS.oneGridRotationHandling(); + } + + + public static boolean oneGridSpecs() { + + return FEATURE_FLAGS.oneGridSpecs(); + } + + + public static boolean predictiveBackToHomeBlur() { + + return FEATURE_FLAGS.predictiveBackToHomeBlur(); + } + + + public static boolean predictiveBackToHomePolish() { + + return FEATURE_FLAGS.predictiveBackToHomePolish(); + } + + public static boolean privateSpaceAddFloatingMaskView() { + return FEATURE_FLAGS.privateSpaceAddFloatingMaskView(); } + public static boolean privateSpaceAnimation() { + return FEATURE_FLAGS.privateSpaceAnimation(); } + public static boolean privateSpaceAppInstallerButton() { + return FEATURE_FLAGS.privateSpaceAppInstallerButton(); } + public static boolean privateSpaceRestrictAccessibilityDrag() { + return FEATURE_FLAGS.privateSpaceRestrictAccessibilityDrag(); } + public static boolean privateSpaceRestrictItemDrag() { + return FEATURE_FLAGS.privateSpaceRestrictItemDrag(); } + public static boolean privateSpaceSysAppsSeparation() { + return FEATURE_FLAGS.privateSpaceSysAppsSeparation(); } + + public static boolean removeAppsRefreshOnRightClick() { + + return FEATURE_FLAGS.removeAppsRefreshOnRightClick(); + } + + + public static boolean removeExcludeFromScreenMagnificationFlagUsage() { + + return FEATURE_FLAGS.removeExcludeFromScreenMagnificationFlagUsage(); + } + + + public static boolean restoreArchivedAppIconsFromDb() { + + return FEATURE_FLAGS.restoreArchivedAppIconsFromDb(); + } + + + public static boolean restoreArchivedShortcuts() { + + return FEATURE_FLAGS.restoreArchivedShortcuts(); + } + + + public static boolean showTaskbarPinningPopupFromAnywhere() { + + return FEATURE_FLAGS.showTaskbarPinningPopupFromAnywhere(); + } + + + public static boolean syncAppLaunchWithTaskbarStash() { + + return FEATURE_FLAGS.syncAppLaunchWithTaskbarStash(); + } + + + public static boolean taskbarOverflow() { + + return FEATURE_FLAGS.taskbarOverflow(); + } + + + public static boolean taskbarQuietModeChangeSupport() { + + return FEATURE_FLAGS.taskbarQuietModeChangeSupport(); + } + + public static boolean useActivityOverlay() { + return FEATURE_FLAGS.useActivityOverlay(); } + + public static boolean useNewIconForArchivedApps() { + + return FEATURE_FLAGS.useNewIconForArchivedApps(); + } + + + public static boolean useSystemRadiusForAppWidgets() { + + return FEATURE_FLAGS.useSystemRadiusForAppWidgets(); + } + + + public static boolean workSchedulerInWorkProfile() { + + return FEATURE_FLAGS.workSchedulerInWorkProfile(); + } + private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); -} \ No newline at end of file +} diff --git a/flags/src/com/android/quickstep/util/DeviceConfigHelper.kt b/flags/src/com/android/quickstep/util/DeviceConfigHelper.kt index a8a2991fd1..d8716a5e8d 100644 --- a/flags/src/com/android/quickstep/util/DeviceConfigHelper.kt +++ b/flags/src/com/android/quickstep/util/DeviceConfigHelper.kt @@ -20,61 +20,72 @@ import android.app.ActivityThread import android.content.Context import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.provider.DeviceConfig +import android.provider.DeviceConfig.OnPropertiesChangedListener +import android.provider.DeviceConfig.Properties import androidx.annotation.WorkerThread +import java.util.concurrent.CopyOnWriteArrayList /** Utility class to manage a set of device configurations */ class DeviceConfigHelper(private val factory: (PropReader) -> ConfigType) { var config: ConfigType private set + private val allKeys: Set + private val propertiesListener = OnPropertiesChangedListener { onDevicePropsChanges(it) } private val sharedPrefChangeListener = OnSharedPreferenceChangeListener { _, _ -> recreateConfig() } - private val changeListeners = mutableListOf() + private val changeListeners = CopyOnWriteArrayList() init { // Initialize the default config once. allKeys = HashSet() - config = factory( - PropReader( - object : PropProvider { - override fun get(key: String, fallback: T): T { - val prefs = prefs - allKeys.add(key) - return when (fallback) { - is Int -> prefs.getInt(key, fallback) as T - is Boolean -> prefs.getBoolean(key, fallback) as T - else -> fallback + config = + factory( + PropReader( + object : PropProvider { + override fun get(key: String, fallback: T): T { + val prefs = prefs + if (fallback is Int) { + allKeys.add(key) + return prefs.getInt(key, fallback) as T + } else if (fallback is Boolean) { + allKeys.add(key) + return prefs.getBoolean(key, fallback) as T + } else return fallback } } - } + ) ) - ) - prefs.registerOnSharedPreferenceChangeListener(sharedPrefChangeListener) } @WorkerThread - private fun onDevicePropsChanges() { + private fun onDevicePropsChanges(properties: Properties) { + if (NAMESPACE_LAUNCHER != properties.namespace) return + if (!allKeys.any(properties.keyset::contains)) return recreateConfig() } private fun recreateConfig() { - config = factory( - PropReader( - object : PropProvider { - override fun get(key: String, fallback: T): T { - return when (fallback) { - is Int -> prefs.getInt(key, fallback) as T - is Boolean -> prefs.getBoolean(key, fallback) as T - else -> fallback + val myProps = + DeviceConfig.getProperties(NAMESPACE_LAUNCHER, *allKeys.toTypedArray()) + config = + factory( + PropReader( + object : PropProvider { + override fun get(key: String, fallback: T): T { + if (fallback is Int) return myProps.getInt(key, fallback) as T + else if (fallback is Boolean) + return myProps.getBoolean(key, fallback) as T + else return fallback } } - } + ) ) - ) } /** Adds a listener for property changes */ @@ -84,6 +95,7 @@ class DeviceConfigHelper(private val factory: (PropReader) -> Config fun removeChangeListener(r: Runnable) = changeListeners.remove(r) fun close() { + DeviceConfig.removeOnPropertiesChangedListener(propertiesListener) prefs.unregisterOnSharedPreferenceChangeListener(sharedPrefChangeListener) } diff --git a/flags/src/com/android/systemui/CustomFeatureFlags.java b/flags/src/com/android/systemui/CustomFeatureFlags.java index bba7a59f65..8dbbac2755 100644 --- a/flags/src/com/android/systemui/CustomFeatureFlags.java +++ b/flags/src/com/android/systemui/CustomFeatureFlags.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Predicate; - /** @hide */ public class CustomFeatureFlags implements FeatureFlags { @@ -17,925 +16,1974 @@ public class CustomFeatureFlags implements FeatureFlags { mGetValueImpl = getValueImpl; } @Override + public boolean activityTransitionUseLargestWindow() { return getValue(Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, - FeatureFlags::activityTransitionUseLargestWindow); + FeatureFlags::activityTransitionUseLargestWindow); } @Override + + public boolean addBlackBackgroundForWindowMagnifier() { + return getValue(Flags.FLAG_ADD_BLACK_BACKGROUND_FOR_WINDOW_MAGNIFIER, + FeatureFlags::addBlackBackgroundForWindowMagnifier); + } + + @Override + + public boolean alwaysComposeQsUiFragment() { + return getValue(Flags.FLAG_ALWAYS_COMPOSE_QS_UI_FRAGMENT, + FeatureFlags::alwaysComposeQsUiFragment); + } + + @Override + public boolean ambientTouchMonitorListenToDisplayChanges() { return getValue(Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, - FeatureFlags::ambientTouchMonitorListenToDisplayChanges); + FeatureFlags::ambientTouchMonitorListenToDisplayChanges); } @Override + public boolean appClipsBacklinks() { return getValue(Flags.FLAG_APP_CLIPS_BACKLINKS, - FeatureFlags::appClipsBacklinks); + FeatureFlags::appClipsBacklinks); } @Override + + public boolean appShortcutRemovalFix() { + return getValue(Flags.FLAG_APP_SHORTCUT_REMOVAL_FIX, + FeatureFlags::appShortcutRemovalFix); + } + + @Override + + public boolean avalancheReplaceHunWhenCritical() { + return getValue(Flags.FLAG_AVALANCHE_REPLACE_HUN_WHEN_CRITICAL, + FeatureFlags::avalancheReplaceHunWhenCritical); + } + + @Override + public boolean bindKeyguardMediaVisibility() { return getValue(Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, - FeatureFlags::bindKeyguardMediaVisibility); + FeatureFlags::bindKeyguardMediaVisibility); } @Override - public boolean bpTalkback() { - return getValue(Flags.FLAG_BP_TALKBACK, - FeatureFlags::bpTalkback); + + public boolean bouncerUiRevamp() { + return getValue(Flags.FLAG_BOUNCER_UI_REVAMP, + FeatureFlags::bouncerUiRevamp); } @Override + + public boolean bouncerUiRevamp2() { + return getValue(Flags.FLAG_BOUNCER_UI_REVAMP_2, + FeatureFlags::bouncerUiRevamp2); + } + + @Override + + public boolean bpColors() { + return getValue(Flags.FLAG_BP_COLORS, + FeatureFlags::bpColors); + } + + @Override + public boolean brightnessSliderFocusState() { return getValue(Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, - FeatureFlags::brightnessSliderFocusState); + FeatureFlags::brightnessSliderFocusState); } @Override - public boolean centralizedStatusBarHeightFix() { - return getValue(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX, - FeatureFlags::centralizedStatusBarHeightFix); + + public boolean checkLockscreenGoneTransition() { + return getValue(Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION, + FeatureFlags::checkLockscreenGoneTransition); } @Override + + public boolean classicFlagsMultiUser() { + return getValue(Flags.FLAG_CLASSIC_FLAGS_MULTI_USER, + FeatureFlags::classicFlagsMultiUser); + } + + @Override + + public boolean clipboardImageTimeout() { + return getValue(Flags.FLAG_CLIPBOARD_IMAGE_TIMEOUT, + FeatureFlags::clipboardImageTimeout); + } + + @Override + public boolean clipboardNoninteractiveOnLockscreen() { return getValue(Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, - FeatureFlags::clipboardNoninteractiveOnLockscreen); + FeatureFlags::clipboardNoninteractiveOnLockscreen); } @Override - public boolean clockReactiveVariants() { - return getValue(Flags.FLAG_CLOCK_REACTIVE_VARIANTS, - FeatureFlags::clockReactiveVariants); + + public boolean clipboardOverlayMultiuser() { + return getValue(Flags.FLAG_CLIPBOARD_OVERLAY_MULTIUSER, + FeatureFlags::clipboardOverlayMultiuser); } @Override + + public boolean clipboardSharedTransitions() { + return getValue(Flags.FLAG_CLIPBOARD_SHARED_TRANSITIONS, + FeatureFlags::clipboardSharedTransitions); + } + + @Override + + public boolean clipboardUseDescriptionMimetype() { + return getValue(Flags.FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE, + FeatureFlags::clipboardUseDescriptionMimetype); + } + + @Override + + public boolean clockFidgetAnimation() { + return getValue(Flags.FLAG_CLOCK_FIDGET_ANIMATION, + FeatureFlags::clockFidgetAnimation); + } + + @Override + public boolean communalBouncerDoNotModifyPluginOpen() { return getValue(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, - FeatureFlags::communalBouncerDoNotModifyPluginOpen); + FeatureFlags::communalBouncerDoNotModifyPluginOpen); } @Override + + public boolean communalEditWidgetsActivityFinishFix() { + return getValue(Flags.FLAG_COMMUNAL_EDIT_WIDGETS_ACTIVITY_FINISH_FIX, + FeatureFlags::communalEditWidgetsActivityFinishFix); + } + + @Override + public boolean communalHub() { return getValue(Flags.FLAG_COMMUNAL_HUB, - FeatureFlags::communalHub); + FeatureFlags::communalHub); } @Override + + public boolean communalHubUseThreadPoolForWidgets() { + return getValue(Flags.FLAG_COMMUNAL_HUB_USE_THREAD_POOL_FOR_WIDGETS, + FeatureFlags::communalHubUseThreadPoolForWidgets); + } + + @Override + + public boolean communalResponsiveGrid() { + return getValue(Flags.FLAG_COMMUNAL_RESPONSIVE_GRID, + FeatureFlags::communalResponsiveGrid); + } + + @Override + + public boolean communalSceneKtfRefactor() { + return getValue(Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + FeatureFlags::communalSceneKtfRefactor); + } + + @Override + + public boolean communalStandaloneSupport() { + return getValue(Flags.FLAG_COMMUNAL_STANDALONE_SUPPORT, + FeatureFlags::communalStandaloneSupport); + } + + @Override + + public boolean communalTimerFlickerFix() { + return getValue(Flags.FLAG_COMMUNAL_TIMER_FLICKER_FIX, + FeatureFlags::communalTimerFlickerFix); + } + + @Override + + public boolean communalWidgetResizing() { + return getValue(Flags.FLAG_COMMUNAL_WIDGET_RESIZING, + FeatureFlags::communalWidgetResizing); + } + + @Override + + public boolean communalWidgetTrampolineFix() { + return getValue(Flags.FLAG_COMMUNAL_WIDGET_TRAMPOLINE_FIX, + FeatureFlags::communalWidgetTrampolineFix); + } + + @Override + public boolean composeBouncer() { return getValue(Flags.FLAG_COMPOSE_BOUNCER, - FeatureFlags::composeBouncer); + FeatureFlags::composeBouncer); } @Override - public boolean composeLockscreen() { - return getValue(Flags.FLAG_COMPOSE_LOCKSCREEN, - FeatureFlags::composeLockscreen); - } - @Override public boolean confineNotificationTouchToViewWidth() { return getValue(Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, - FeatureFlags::confineNotificationTouchToViewWidth); + FeatureFlags::confineNotificationTouchToViewWidth); } @Override - public boolean constraintBp() { - return getValue(Flags.FLAG_CONSTRAINT_BP, - FeatureFlags::constraintBp); + + public boolean contAuthPlugin() { + return getValue(Flags.FLAG_CONT_AUTH_PLUGIN, + FeatureFlags::contAuthPlugin); } @Override + public boolean contextualTipsAssistantDismissFix() { return getValue(Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, - FeatureFlags::contextualTipsAssistantDismissFix); + FeatureFlags::contextualTipsAssistantDismissFix); } @Override + public boolean coroutineTracing() { return getValue(Flags.FLAG_COROUTINE_TRACING, - FeatureFlags::coroutineTracing); + FeatureFlags::coroutineTracing); } @Override + public boolean createWindowlessWindowMagnifier() { return getValue(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, - FeatureFlags::createWindowlessWindowMagnifier); + FeatureFlags::createWindowlessWindowMagnifier); } @Override - public boolean dedicatedNotifInflationThread() { - return getValue(Flags.FLAG_DEDICATED_NOTIF_INFLATION_THREAD, - FeatureFlags::dedicatedNotifInflationThread); + + public boolean debugLiveUpdatesPromoteAll() { + return getValue(Flags.FLAG_DEBUG_LIVE_UPDATES_PROMOTE_ALL, + FeatureFlags::debugLiveUpdatesPromoteAll); } @Override + + public boolean decoupleViewControllerInAnimlib() { + return getValue(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB, + FeatureFlags::decoupleViewControllerInAnimlib); + } + + @Override + public boolean delayShowMagnificationButton() { return getValue(Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, - FeatureFlags::delayShowMagnificationButton); + FeatureFlags::delayShowMagnificationButton); } @Override - public boolean delayedWakelockReleaseOnBackgroundThread() { - return getValue(Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD, - FeatureFlags::delayedWakelockReleaseOnBackgroundThread); + + public boolean desktopEffectsQsTile() { + return getValue(Flags.FLAG_DESKTOP_EFFECTS_QS_TILE, + FeatureFlags::desktopEffectsQsTile); } @Override + public boolean deviceEntryUdfpsRefactor() { return getValue(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, - FeatureFlags::deviceEntryUdfpsRefactor); + FeatureFlags::deviceEntryUdfpsRefactor); } @Override + + public boolean disableBlurredShadeVisible() { + return getValue(Flags.FLAG_DISABLE_BLURRED_SHADE_VISIBLE, + FeatureFlags::disableBlurredShadeVisible); + } + + @Override + public boolean disableContextualTipsFrequencyCheck() { return getValue(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, - FeatureFlags::disableContextualTipsFrequencyCheck); + FeatureFlags::disableContextualTipsFrequencyCheck); } @Override + public boolean disableContextualTipsIosSwitcherCheck() { return getValue(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, - FeatureFlags::disableContextualTipsIosSwitcherCheck); + FeatureFlags::disableContextualTipsIosSwitcherCheck); } @Override - public boolean dozeuiSchedulingAlarmsBackgroundExecution() { - return getValue(Flags.FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION, - FeatureFlags::dozeuiSchedulingAlarmsBackgroundExecution); + + public boolean disableShadeTrackpadTwoFingerSwipe() { + return getValue(Flags.FLAG_DISABLE_SHADE_TRACKPAD_TWO_FINGER_SWIPE, + FeatureFlags::disableShadeTrackpadTwoFingerSwipe); } @Override + + public boolean doubleTapToSleep() { + return getValue(Flags.FLAG_DOUBLE_TAP_TO_SLEEP, + FeatureFlags::doubleTapToSleep); + } + + @Override + public boolean dreamInputSessionPilferOnce() { return getValue(Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, - FeatureFlags::dreamInputSessionPilferOnce); + FeatureFlags::dreamInputSessionPilferOnce); } @Override + public boolean dreamOverlayBouncerSwipeDirectionFiltering() { return getValue(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, - FeatureFlags::dreamOverlayBouncerSwipeDirectionFiltering); + FeatureFlags::dreamOverlayBouncerSwipeDirectionFiltering); } @Override - public boolean dualShade() { - return getValue(Flags.FLAG_DUAL_SHADE, - FeatureFlags::dualShade); + + public boolean dreamOverlayUpdatedFont() { + return getValue(Flags.FLAG_DREAM_OVERLAY_UPDATED_FONT, + FeatureFlags::dreamOverlayUpdatedFont); } @Override + public boolean edgeBackGestureHandlerThread() { return getValue(Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, - FeatureFlags::edgeBackGestureHandlerThread); + FeatureFlags::edgeBackGestureHandlerThread); } @Override + public boolean edgebackGestureHandlerGetRunningTasksBackground() { return getValue(Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, - FeatureFlags::edgebackGestureHandlerGetRunningTasksBackground); + FeatureFlags::edgebackGestureHandlerGetRunningTasksBackground); } @Override + public boolean enableBackgroundKeyguardOndrawnCallback() { return getValue(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, - FeatureFlags::enableBackgroundKeyguardOndrawnCallback); + FeatureFlags::enableBackgroundKeyguardOndrawnCallback); } @Override + public boolean enableContextualTipForMuteVolume() { return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, - FeatureFlags::enableContextualTipForMuteVolume); + FeatureFlags::enableContextualTipForMuteVolume); } @Override + public boolean enableContextualTipForPowerOff() { return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, - FeatureFlags::enableContextualTipForPowerOff); + FeatureFlags::enableContextualTipForPowerOff); } @Override + public boolean enableContextualTipForTakeScreenshot() { return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, - FeatureFlags::enableContextualTipForTakeScreenshot); + FeatureFlags::enableContextualTipForTakeScreenshot); } @Override + public boolean enableContextualTips() { return getValue(Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, - FeatureFlags::enableContextualTips); + FeatureFlags::enableContextualTips); } @Override + public boolean enableEfficientDisplayRepository() { return getValue(Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, - FeatureFlags::enableEfficientDisplayRepository); + FeatureFlags::enableEfficientDisplayRepository); } @Override + public boolean enableLayoutTracing() { return getValue(Flags.FLAG_ENABLE_LAYOUT_TRACING, - FeatureFlags::enableLayoutTracing); + FeatureFlags::enableLayoutTracing); } @Override + + public boolean enableUnderlay() { + return getValue(Flags.FLAG_ENABLE_UNDERLAY, + FeatureFlags::enableUnderlay); + } + + @Override + public boolean enableViewCaptureTracing() { return getValue(Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, - FeatureFlags::enableViewCaptureTracing); + FeatureFlags::enableViewCaptureTracing); } @Override - public boolean enableWidgetPickerSizeFilter() { - return getValue(Flags.FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER, - FeatureFlags::enableWidgetPickerSizeFilter); - } - @Override public boolean enforceBrightnessBaseUserRestriction() { return getValue(Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, - FeatureFlags::enforceBrightnessBaseUserRestriction); + FeatureFlags::enforceBrightnessBaseUserRestriction); } @Override + public boolean exampleFlag() { return getValue(Flags.FLAG_EXAMPLE_FLAG, - FeatureFlags::exampleFlag); + FeatureFlags::exampleFlag); } @Override - public boolean fastUnlockTransition() { - return getValue(Flags.FLAG_FAST_UNLOCK_TRANSITION, - FeatureFlags::fastUnlockTransition); + + public boolean expandCollapsePrivacyDialog() { + return getValue(Flags.FLAG_EXPAND_COLLAPSE_PRIVACY_DIALOG, + FeatureFlags::expandCollapsePrivacyDialog); } @Override + + public boolean expandHeadsUpOnInlineReply() { + return getValue(Flags.FLAG_EXPAND_HEADS_UP_ON_INLINE_REPLY, + FeatureFlags::expandHeadsUpOnInlineReply); + } + + @Override + + public boolean expandedPrivacyIndicatorsOnLargeScreen() { + return getValue(Flags.FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN, + FeatureFlags::expandedPrivacyIndicatorsOnLargeScreen); + } + + @Override + + public boolean extendedAppsShortcutCategory() { + return getValue(Flags.FLAG_EXTENDED_APPS_SHORTCUT_CATEGORY, + FeatureFlags::extendedAppsShortcutCategory); + } + + @Override + + public boolean faceMessageDeferUpdate() { + return getValue(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE, + FeatureFlags::faceMessageDeferUpdate); + } + + @Override + + public boolean faceScanningAnimationNpeFix() { + return getValue(Flags.FLAG_FACE_SCANNING_ANIMATION_NPE_FIX, + FeatureFlags::faceScanningAnimationNpeFix); + } + + @Override + + public boolean fasterUnlockTransition() { + return getValue(Flags.FLAG_FASTER_UNLOCK_TRANSITION, + FeatureFlags::fasterUnlockTransition); + } + + @Override + + public boolean fetchBookmarksXmlKeyboardShortcuts() { + return getValue(Flags.FLAG_FETCH_BOOKMARKS_XML_KEYBOARD_SHORTCUTS, + FeatureFlags::fetchBookmarksXmlKeyboardShortcuts); + } + + @Override + public boolean fixImageWallpaperCrashSurfaceAlreadyReleased() { return getValue(Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, - FeatureFlags::fixImageWallpaperCrashSurfaceAlreadyReleased); + FeatureFlags::fixImageWallpaperCrashSurfaceAlreadyReleased); } @Override + public boolean fixScreenshotActionDismissSystemWindows() { return getValue(Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, - FeatureFlags::fixScreenshotActionDismissSystemWindows); + FeatureFlags::fixScreenshotActionDismissSystemWindows); } @Override + public boolean floatingMenuAnimatedTuck() { return getValue(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, - FeatureFlags::floatingMenuAnimatedTuck); + FeatureFlags::floatingMenuAnimatedTuck); } @Override + + public boolean floatingMenuDisplayCutoutSupport() { + return getValue(Flags.FLAG_FLOATING_MENU_DISPLAY_CUTOUT_SUPPORT, + FeatureFlags::floatingMenuDisplayCutoutSupport); + } + + @Override + public boolean floatingMenuDragToEdit() { return getValue(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, - FeatureFlags::floatingMenuDragToEdit); + FeatureFlags::floatingMenuDragToEdit); } @Override + public boolean floatingMenuDragToHide() { return getValue(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, - FeatureFlags::floatingMenuDragToHide); + FeatureFlags::floatingMenuDragToHide); } @Override + + public boolean floatingMenuHearingDeviceStatusIcon() { + return getValue(Flags.FLAG_FLOATING_MENU_HEARING_DEVICE_STATUS_ICON, + FeatureFlags::floatingMenuHearingDeviceStatusIcon); + } + + @Override + public boolean floatingMenuImeDisplacementAnimation() { return getValue(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, - FeatureFlags::floatingMenuImeDisplacementAnimation); + FeatureFlags::floatingMenuImeDisplacementAnimation); } @Override + public boolean floatingMenuNarrowTargetContentObserver() { return getValue(Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, - FeatureFlags::floatingMenuNarrowTargetContentObserver); + FeatureFlags::floatingMenuNarrowTargetContentObserver); } @Override + + public boolean floatingMenuNotifyTargetsChangedOnStrictDiff() { + return getValue(Flags.FLAG_FLOATING_MENU_NOTIFY_TARGETS_CHANGED_ON_STRICT_DIFF, + FeatureFlags::floatingMenuNotifyTargetsChangedOnStrictDiff); + } + + @Override + public boolean floatingMenuOverlapsNavBarsFlag() { return getValue(Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, - FeatureFlags::floatingMenuOverlapsNavBarsFlag); + FeatureFlags::floatingMenuOverlapsNavBarsFlag); } @Override + public boolean floatingMenuRadiiAnimation() { return getValue(Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, - FeatureFlags::floatingMenuRadiiAnimation); - } - - @Override - public boolean generatedPreviews() { - return getValue(Flags.FLAG_GENERATED_PREVIEWS, - FeatureFlags::generatedPreviews); + FeatureFlags::floatingMenuRadiiAnimation); } @Override + public boolean getConnectedDeviceNameUnsynchronized() { return getValue(Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, - FeatureFlags::getConnectedDeviceNameUnsynchronized); + FeatureFlags::getConnectedDeviceNameUnsynchronized); } @Override + public boolean glanceableHubAllowKeyguardWhenDreaming() { return getValue(Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, - FeatureFlags::glanceableHubAllowKeyguardWhenDreaming); + FeatureFlags::glanceableHubAllowKeyguardWhenDreaming); } @Override - public boolean glanceableHubFullscreenSwipe() { - return getValue(Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE, - FeatureFlags::glanceableHubFullscreenSwipe); + + public boolean glanceableHubBlurredBackground() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND, + FeatureFlags::glanceableHubBlurredBackground); } @Override - public boolean glanceableHubGestureHandle() { - return getValue(Flags.FLAG_GLANCEABLE_HUB_GESTURE_HANDLE, - FeatureFlags::glanceableHubGestureHandle); + + public boolean glanceableHubDirectEditMode() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE, + FeatureFlags::glanceableHubDirectEditMode); } @Override - public boolean glanceableHubShortcutButton() { - return getValue(Flags.FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON, - FeatureFlags::glanceableHubShortcutButton); + + public boolean glanceableHubV2() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_V2, + FeatureFlags::glanceableHubV2); } @Override - public boolean hapticBrightnessSlider() { - return getValue(Flags.FLAG_HAPTIC_BRIGHTNESS_SLIDER, - FeatureFlags::hapticBrightnessSlider); + + public boolean glanceableHubV2Resources() { + return getValue(Flags.FLAG_GLANCEABLE_HUB_V2_RESOURCES, + FeatureFlags::glanceableHubV2Resources); } @Override - public boolean hapticVolumeSlider() { - return getValue(Flags.FLAG_HAPTIC_VOLUME_SLIDER, - FeatureFlags::hapticVolumeSlider); + + public boolean hapticsForComposeSliders() { + return getValue(Flags.FLAG_HAPTICS_FOR_COMPOSE_SLIDERS, + FeatureFlags::hapticsForComposeSliders); } @Override + + public boolean hardwareColorStyles() { + return getValue(Flags.FLAG_HARDWARE_COLOR_STYLES, + FeatureFlags::hardwareColorStyles); + } + + @Override + public boolean hearingAidsQsTileDialog() { return getValue(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, - FeatureFlags::hearingAidsQsTileDialog); + FeatureFlags::hearingAidsQsTileDialog); } @Override + public boolean hearingDevicesDialogRelatedTools() { return getValue(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, - FeatureFlags::hearingDevicesDialogRelatedTools); + FeatureFlags::hearingDevicesDialogRelatedTools); } @Override + + public boolean hideRingerButtonInSingleVolumeMode() { + return getValue(Flags.FLAG_HIDE_RINGER_BUTTON_IN_SINGLE_VOLUME_MODE, + FeatureFlags::hideRingerButtonInSingleVolumeMode); + } + + @Override + + public boolean homeControlsDreamHsum() { + return getValue(Flags.FLAG_HOME_CONTROLS_DREAM_HSUM, + FeatureFlags::homeControlsDreamHsum); + } + + @Override + + public boolean hubEditModeTouchAdjustments() { + return getValue(Flags.FLAG_HUB_EDIT_MODE_TOUCH_ADJUSTMENTS, + FeatureFlags::hubEditModeTouchAdjustments); + } + + @Override + + public boolean hubmodeFullscreenVerticalSwipe() { + return getValue(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE, + FeatureFlags::hubmodeFullscreenVerticalSwipe); + } + + @Override + + public boolean hubmodeFullscreenVerticalSwipeFix() { + return getValue(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, + FeatureFlags::hubmodeFullscreenVerticalSwipeFix); + } + + @Override + + public boolean iconRefresh2025() { + return getValue(Flags.FLAG_ICON_REFRESH_2025, + FeatureFlags::iconRefresh2025); + } + + @Override + + public boolean ignoreTouchesNextToNotificationShelf() { + return getValue(Flags.FLAG_IGNORE_TOUCHES_NEXT_TO_NOTIFICATION_SHELF, + FeatureFlags::ignoreTouchesNextToNotificationShelf); + } + + @Override + + public boolean indicationTextA11yFix() { + return getValue(Flags.FLAG_INDICATION_TEXT_A11Y_FIX, + FeatureFlags::indicationTextA11yFix); + } + + @Override + public boolean keyboardDockingIndicator() { return getValue(Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, - FeatureFlags::keyboardDockingIndicator); + FeatureFlags::keyboardDockingIndicator); } @Override + public boolean keyboardShortcutHelperRewrite() { return getValue(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, - FeatureFlags::keyboardShortcutHelperRewrite); + FeatureFlags::keyboardShortcutHelperRewrite); } @Override - public boolean keyguardBottomAreaRefactor() { - return getValue(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, - FeatureFlags::keyguardBottomAreaRefactor); + + public boolean keyboardShortcutHelperShortcutCustomizer() { + return getValue(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_SHORTCUT_CUSTOMIZER, + FeatureFlags::keyboardShortcutHelperShortcutCustomizer); } @Override + + public boolean keyboardTouchpadContextualEducation() { + return getValue(Flags.FLAG_KEYBOARD_TOUCHPAD_CONTEXTUAL_EDUCATION, + FeatureFlags::keyboardTouchpadContextualEducation); + } + + @Override + + public boolean keyguardTransitionForceFinishOnScreenOff() { + return getValue(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF, + FeatureFlags::keyguardTransitionForceFinishOnScreenOff); + } + + @Override + + public boolean keyguardWmReorderAtmsCalls() { + return getValue(Flags.FLAG_KEYGUARD_WM_REORDER_ATMS_CALLS, + FeatureFlags::keyguardWmReorderAtmsCalls); + } + + @Override + public boolean keyguardWmStateRefactor() { return getValue(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, - FeatureFlags::keyguardWmStateRefactor); + FeatureFlags::keyguardWmStateRefactor); } @Override - public boolean lightRevealMigration() { - return getValue(Flags.FLAG_LIGHT_REVEAL_MIGRATION, - FeatureFlags::lightRevealMigration); + + public boolean lockscreenFont() { + return getValue(Flags.FLAG_LOCKSCREEN_FONT, + FeatureFlags::lockscreenFont); } @Override + + public boolean lowLightClockDream() { + return getValue(Flags.FLAG_LOW_LIGHT_CLOCK_DREAM, + FeatureFlags::lowLightClockDream); + } + + @Override + + public boolean magneticNotificationSwipes() { + return getValue(Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES, + FeatureFlags::magneticNotificationSwipes); + } + + @Override + + public boolean mediaControlsA11yColors() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_A11Y_COLORS, + FeatureFlags::mediaControlsA11yColors); + } + + @Override + + public boolean mediaControlsButtonMedia3() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3, + FeatureFlags::mediaControlsButtonMedia3); + } + + @Override + + public boolean mediaControlsButtonMedia3Placement() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3_PLACEMENT, + FeatureFlags::mediaControlsButtonMedia3Placement); + } + + @Override + + public boolean mediaControlsDeviceManagerBackgroundExecution() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION, + FeatureFlags::mediaControlsDeviceManagerBackgroundExecution); + } + + @Override + + public boolean mediaControlsDrawablesReuseBugfix() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE_BUGFIX, + FeatureFlags::mediaControlsDrawablesReuseBugfix); + } + + @Override + public boolean mediaControlsLockscreenShadeBugFix() { return getValue(Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, - FeatureFlags::mediaControlsLockscreenShadeBugFix); + FeatureFlags::mediaControlsLockscreenShadeBugFix); } @Override - public boolean mediaControlsRefactor() { - return getValue(Flags.FLAG_MEDIA_CONTROLS_REFACTOR, - FeatureFlags::mediaControlsRefactor); + + public boolean mediaControlsUiUpdate() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_UI_UPDATE, + FeatureFlags::mediaControlsUiUpdate); } @Override + + public boolean mediaControlsUmoInflationInBackground() { + return getValue(Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND, + FeatureFlags::mediaControlsUmoInflationInBackground); + } + + @Override + public boolean mediaControlsUserInitiatedDeleteintent() { return getValue(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, - FeatureFlags::mediaControlsUserInitiatedDeleteintent); + FeatureFlags::mediaControlsUserInitiatedDeleteintent); } @Override - public boolean migrateClocksToBlueprint() { - return getValue(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - FeatureFlags::migrateClocksToBlueprint); + + public boolean mediaLoadMetadataViaMediaDataLoader() { + return getValue(Flags.FLAG_MEDIA_LOAD_METADATA_VIA_MEDIA_DATA_LOADER, + FeatureFlags::mediaLoadMetadataViaMediaDataLoader); } @Override + + public boolean mediaLockscreenLaunchAnimation() { + return getValue(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION, + FeatureFlags::mediaLockscreenLaunchAnimation); + } + + @Override + + public boolean mediaProjectionDialogBehindLockscreen() { + return getValue(Flags.FLAG_MEDIA_PROJECTION_DIALOG_BEHIND_LOCKSCREEN, + FeatureFlags::mediaProjectionDialogBehindLockscreen); + } + + @Override + + public boolean mediaProjectionGreyErrorText() { + return getValue(Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT, + FeatureFlags::mediaProjectionGreyErrorText); + } + + @Override + + public boolean mediaProjectionRequestAttributionFix() { + return getValue(Flags.FLAG_MEDIA_PROJECTION_REQUEST_ATTRIBUTION_FIX, + FeatureFlags::mediaProjectionRequestAttributionFix); + } + + @Override + + public boolean modesUiDialogPaging() { + return getValue(Flags.FLAG_MODES_UI_DIALOG_PAGING, + FeatureFlags::modesUiDialogPaging); + } + + @Override + + public boolean moveTransitionAnimationLayer() { + return getValue(Flags.FLAG_MOVE_TRANSITION_ANIMATION_LAYER, + FeatureFlags::moveTransitionAnimationLayer); + } + + @Override + + public boolean msdlFeedback() { + return getValue(Flags.FLAG_MSDL_FEEDBACK, + FeatureFlags::msdlFeedback); + } + + @Override + + public boolean multiuserWifiPickerTrackerSupport() { + return getValue(Flags.FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT, + FeatureFlags::multiuserWifiPickerTrackerSupport); + } + + @Override + public boolean newAodTransition() { return getValue(Flags.FLAG_NEW_AOD_TRANSITION, - FeatureFlags::newAodTransition); + FeatureFlags::newAodTransition); } @Override - public boolean newTouchpadGesturesTutorial() { - return getValue(Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, - FeatureFlags::newTouchpadGesturesTutorial); - } - @Override public boolean newVolumePanel() { return getValue(Flags.FLAG_NEW_VOLUME_PANEL, - FeatureFlags::newVolumePanel); + FeatureFlags::newVolumePanel); } @Override + + public boolean nonTouchscreenDevicesBypassFalsing() { + return getValue(Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING, + FeatureFlags::nonTouchscreenDevicesBypassFalsing); + } + + @Override + + public boolean notesRoleQsTile() { + return getValue(Flags.FLAG_NOTES_ROLE_QS_TILE, + FeatureFlags::notesRoleQsTile); + } + + @Override + + public boolean notificationAddXOnHoverToDismiss() { + return getValue(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS, + FeatureFlags::notificationAddXOnHoverToDismiss); + } + + @Override + + public boolean notificationAmbientSuppressionAfterInflation() { + return getValue(Flags.FLAG_NOTIFICATION_AMBIENT_SUPPRESSION_AFTER_INFLATION, + FeatureFlags::notificationAmbientSuppressionAfterInflation); + } + + @Override + + public boolean notificationAnimatedActionsTreatment() { + return getValue(Flags.FLAG_NOTIFICATION_ANIMATED_ACTIONS_TREATMENT, + FeatureFlags::notificationAnimatedActionsTreatment); + } + + @Override + + public boolean notificationAppearNonlinear() { + return getValue(Flags.FLAG_NOTIFICATION_APPEAR_NONLINEAR, + FeatureFlags::notificationAppearNonlinear); + } + + @Override + public boolean notificationAsyncGroupHeaderInflation() { return getValue(Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, - FeatureFlags::notificationAsyncGroupHeaderInflation); + FeatureFlags::notificationAsyncGroupHeaderInflation); } @Override + public boolean notificationAsyncHybridViewInflation() { return getValue(Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, - FeatureFlags::notificationAsyncHybridViewInflation); + FeatureFlags::notificationAsyncHybridViewInflation); } @Override + public boolean notificationAvalancheSuppression() { return getValue(Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, - FeatureFlags::notificationAvalancheSuppression); + FeatureFlags::notificationAvalancheSuppression); } @Override + public boolean notificationAvalancheThrottleHun() { return getValue(Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, - FeatureFlags::notificationAvalancheThrottleHun); + FeatureFlags::notificationAvalancheThrottleHun); } @Override + public boolean notificationBackgroundTintOptimization() { return getValue(Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, - FeatureFlags::notificationBackgroundTintOptimization); + FeatureFlags::notificationBackgroundTintOptimization); } @Override + + public boolean notificationBundleUi() { + return getValue(Flags.FLAG_NOTIFICATION_BUNDLE_UI, + FeatureFlags::notificationBundleUi); + } + + @Override + public boolean notificationColorUpdateLogger() { return getValue(Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, - FeatureFlags::notificationColorUpdateLogger); + FeatureFlags::notificationColorUpdateLogger); } @Override + public boolean notificationContentAlphaOptimization() { return getValue(Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, - FeatureFlags::notificationContentAlphaOptimization); + FeatureFlags::notificationContentAlphaOptimization); } @Override + public boolean notificationFooterBackgroundTintOptimization() { return getValue(Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, - FeatureFlags::notificationFooterBackgroundTintOptimization); + FeatureFlags::notificationFooterBackgroundTintOptimization); } @Override - public boolean notificationMediaManagerBackgroundExecution() { - return getValue(Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION, - FeatureFlags::notificationMediaManagerBackgroundExecution); - } - @Override - public boolean notificationMinimalismPrototype() { - return getValue(Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE, - FeatureFlags::notificationMinimalismPrototype); - } - - @Override public boolean notificationOverExpansionClippingFix() { return getValue(Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, - FeatureFlags::notificationOverExpansionClippingFix); + FeatureFlags::notificationOverExpansionClippingFix); } @Override - public boolean notificationPulsingFix() { - return getValue(Flags.FLAG_NOTIFICATION_PULSING_FIX, - FeatureFlags::notificationPulsingFix); + + public boolean notificationReentrantDismiss() { + return getValue(Flags.FLAG_NOTIFICATION_REENTRANT_DISMISS, + FeatureFlags::notificationReentrantDismiss); } @Override + + public boolean notificationRowAccessibilityExpanded() { + return getValue(Flags.FLAG_NOTIFICATION_ROW_ACCESSIBILITY_EXPANDED, + FeatureFlags::notificationRowAccessibilityExpanded); + } + + @Override + public boolean notificationRowContentBinderRefactor() { return getValue(Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, - FeatureFlags::notificationRowContentBinderRefactor); + FeatureFlags::notificationRowContentBinderRefactor); } @Override + + public boolean notificationRowTransparency() { + return getValue(Flags.FLAG_NOTIFICATION_ROW_TRANSPARENCY, + FeatureFlags::notificationRowTransparency); + } + + @Override + public boolean notificationRowUserContext() { return getValue(Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, - FeatureFlags::notificationRowUserContext); + FeatureFlags::notificationRowUserContext); } @Override + + public boolean notificationShadeBlur() { + return getValue(Flags.FLAG_NOTIFICATION_SHADE_BLUR, + FeatureFlags::notificationShadeBlur); + } + + @Override + + public boolean notificationShadeUiThread() { + return getValue(Flags.FLAG_NOTIFICATION_SHADE_UI_THREAD, + FeatureFlags::notificationShadeUiThread); + } + + @Override + + public boolean notificationSkipSilentUpdates() { + return getValue(Flags.FLAG_NOTIFICATION_SKIP_SILENT_UPDATES, + FeatureFlags::notificationSkipSilentUpdates); + } + + @Override + + public boolean notificationTransparentHeaderFix() { + return getValue(Flags.FLAG_NOTIFICATION_TRANSPARENT_HEADER_FIX, + FeatureFlags::notificationTransparentHeaderFix); + } + + @Override + public boolean notificationViewFlipperPausingV2() { return getValue(Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, - FeatureFlags::notificationViewFlipperPausingV2); + FeatureFlags::notificationViewFlipperPausingV2); } @Override + public boolean notificationsBackgroundIcons() { return getValue(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, - FeatureFlags::notificationsBackgroundIcons); + FeatureFlags::notificationsBackgroundIcons); } @Override - public boolean notificationsFooterViewRefactor() { - return getValue(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR, - FeatureFlags::notificationsFooterViewRefactor); + + public boolean notificationsFooterVisibilityFix() { + return getValue(Flags.FLAG_NOTIFICATIONS_FOOTER_VISIBILITY_FIX, + FeatureFlags::notificationsFooterVisibilityFix); } @Override - public boolean notificationsHeadsUpRefactor() { - return getValue(Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR, - FeatureFlags::notificationsHeadsUpRefactor); - } - @Override public boolean notificationsHideOnDisplaySwitch() { return getValue(Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, - FeatureFlags::notificationsHideOnDisplaySwitch); + FeatureFlags::notificationsHideOnDisplaySwitch); } @Override + + public boolean notificationsHunSharedAnimationValues() { + return getValue(Flags.FLAG_NOTIFICATIONS_HUN_SHARED_ANIMATION_VALUES, + FeatureFlags::notificationsHunSharedAnimationValues); + } + + @Override + public boolean notificationsIconContainerRefactor() { return getValue(Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, - FeatureFlags::notificationsIconContainerRefactor); + FeatureFlags::notificationsIconContainerRefactor); } @Override - public boolean notificationsImprovedHunAnimation() { - return getValue(Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION, - FeatureFlags::notificationsImprovedHunAnimation); + + public boolean notificationsLaunchRadius() { + return getValue(Flags.FLAG_NOTIFICATIONS_LAUNCH_RADIUS, + FeatureFlags::notificationsLaunchRadius); } @Override + public boolean notificationsLiveDataStoreRefactor() { return getValue(Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, - FeatureFlags::notificationsLiveDataStoreRefactor); + FeatureFlags::notificationsLiveDataStoreRefactor); } @Override + + public boolean notificationsPinnedHunInShade() { + return getValue(Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE, + FeatureFlags::notificationsPinnedHunInShade); + } + + @Override + + public boolean notificationsRedesignFooterView() { + return getValue(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW, + FeatureFlags::notificationsRedesignFooterView); + } + + @Override + + public boolean notificationsRedesignGuts() { + return getValue(Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS, + FeatureFlags::notificationsRedesignGuts); + } + + @Override + + public boolean notifyPasswordTextViewUserActivityInBackground() { + return getValue(Flags.FLAG_NOTIFY_PASSWORD_TEXT_VIEW_USER_ACTIVITY_IN_BACKGROUND, + FeatureFlags::notifyPasswordTextViewUserActivityInBackground); + } + + @Override + public boolean notifyPowerManagerUserActivityBackground() { return getValue(Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, - FeatureFlags::notifyPowerManagerUserActivityBackground); + FeatureFlags::notifyPowerManagerUserActivityBackground); } @Override + + public boolean onlyShowMediaStreamSliderInSingleVolumeMode() { + return getValue(Flags.FLAG_ONLY_SHOW_MEDIA_STREAM_SLIDER_IN_SINGLE_VOLUME_MODE, + FeatureFlags::onlyShowMediaStreamSliderInSingleVolumeMode); + } + + @Override + + public boolean outputSwitcherRedesign() { + return getValue(Flags.FLAG_OUTPUT_SWITCHER_REDESIGN, + FeatureFlags::outputSwitcherRedesign); + } + + @Override + + public boolean overrideSuppressOverlayCondition() { + return getValue(Flags.FLAG_OVERRIDE_SUPPRESS_OVERLAY_CONDITION, + FeatureFlags::overrideSuppressOverlayCondition); + } + + @Override + + public boolean permissionHelperInlineUiRichOngoing() { + return getValue(Flags.FLAG_PERMISSION_HELPER_INLINE_UI_RICH_ONGOING, + FeatureFlags::permissionHelperInlineUiRichOngoing); + } + + @Override + + public boolean permissionHelperUiRichOngoing() { + return getValue(Flags.FLAG_PERMISSION_HELPER_UI_RICH_ONGOING, + FeatureFlags::permissionHelperUiRichOngoing); + } + + @Override + + public boolean physicalNotificationMovement() { + return getValue(Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT, + FeatureFlags::physicalNotificationMovement); + } + + @Override + public boolean pinInputFieldStyledFocusState() { return getValue(Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, - FeatureFlags::pinInputFieldStyledFocusState); + FeatureFlags::pinInputFieldStyledFocusState); } @Override - public boolean predictiveBackAnimateBouncer() { - return getValue(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER, - FeatureFlags::predictiveBackAnimateBouncer); - } - @Override - public boolean predictiveBackAnimateDialogs() { - return getValue(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS, - FeatureFlags::predictiveBackAnimateDialogs); - } - - @Override public boolean predictiveBackAnimateShade() { return getValue(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, - FeatureFlags::predictiveBackAnimateShade); + FeatureFlags::predictiveBackAnimateShade); } @Override - public boolean predictiveBackSysui() { - return getValue(Flags.FLAG_PREDICTIVE_BACK_SYSUI, - FeatureFlags::predictiveBackSysui); + + public boolean predictiveBackDelayWmTransition() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_DELAY_WM_TRANSITION, + FeatureFlags::predictiveBackDelayWmTransition); } @Override + public boolean priorityPeopleSection() { return getValue(Flags.FLAG_PRIORITY_PEOPLE_SECTION, - FeatureFlags::priorityPeopleSection); + FeatureFlags::priorityPeopleSection); } @Override - public boolean privacyDotUnfoldWrongCornerFix() { - return getValue(Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX, - FeatureFlags::privacyDotUnfoldWrongCornerFix); + + public boolean promoteNotificationsAutomatically() { + return getValue(Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, + FeatureFlags::promoteNotificationsAutomatically); } @Override - public boolean pssAppSelectorAbruptExitFix() { - return getValue(Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX, - FeatureFlags::pssAppSelectorAbruptExitFix); - } - @Override public boolean pssAppSelectorRecentsSplitScreen() { return getValue(Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, - FeatureFlags::pssAppSelectorRecentsSplitScreen); + FeatureFlags::pssAppSelectorRecentsSplitScreen); } @Override + public boolean pssTaskSwitcher() { return getValue(Flags.FLAG_PSS_TASK_SWITCHER, - FeatureFlags::pssTaskSwitcher); + FeatureFlags::pssTaskSwitcher); } @Override + public boolean qsCustomTileClickGuaranteedBugFix() { return getValue(Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, - FeatureFlags::qsCustomTileClickGuaranteedBugFix); + FeatureFlags::qsCustomTileClickGuaranteedBugFix); } @Override - public boolean qsNewPipeline() { - return getValue(Flags.FLAG_QS_NEW_PIPELINE, - FeatureFlags::qsNewPipeline); - } - @Override public boolean qsNewTiles() { return getValue(Flags.FLAG_QS_NEW_TILES, - FeatureFlags::qsNewTiles); + FeatureFlags::qsNewTiles); } @Override + public boolean qsNewTilesFuture() { return getValue(Flags.FLAG_QS_NEW_TILES_FUTURE, - FeatureFlags::qsNewTilesFuture); + FeatureFlags::qsNewTilesFuture); } @Override + + public boolean qsQuickRebindActiveTiles() { + return getValue(Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES, + FeatureFlags::qsQuickRebindActiveTiles); + } + + @Override + + public boolean qsRegisterSettingObserverOnBgThread() { + return getValue(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD, + FeatureFlags::qsRegisterSettingObserverOnBgThread); + } + + @Override + + public boolean qsTileDetailedView() { + return getValue(Flags.FLAG_QS_TILE_DETAILED_VIEW, + FeatureFlags::qsTileDetailedView); + } + + @Override + public boolean qsTileFocusState() { return getValue(Flags.FLAG_QS_TILE_FOCUS_STATE, - FeatureFlags::qsTileFocusState); + FeatureFlags::qsTileFocusState); } @Override + public boolean qsUiRefactor() { return getValue(Flags.FLAG_QS_UI_REFACTOR, - FeatureFlags::qsUiRefactor); + FeatureFlags::qsUiRefactor); } @Override - public boolean quickSettingsVisualHapticsLongpress() { - return getValue(Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS, - FeatureFlags::quickSettingsVisualHapticsLongpress); + + public boolean qsUiRefactorComposeFragment() { + return getValue(Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT, + FeatureFlags::qsUiRefactorComposeFragment); } @Override + public boolean recordIssueQsTile() { return getValue(Flags.FLAG_RECORD_ISSUE_QS_TILE, - FeatureFlags::recordIssueQsTile); + FeatureFlags::recordIssueQsTile); } @Override + + public boolean redesignMagnificationWindowSize() { + return getValue(Flags.FLAG_REDESIGN_MAGNIFICATION_WINDOW_SIZE, + FeatureFlags::redesignMagnificationWindowSize); + } + + @Override + public boolean refactorGetCurrentUser() { return getValue(Flags.FLAG_REFACTOR_GET_CURRENT_USER, - FeatureFlags::refactorGetCurrentUser); + FeatureFlags::refactorGetCurrentUser); } @Override + public boolean registerBatteryControllerReceiversInCorestartable() { return getValue(Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, - FeatureFlags::registerBatteryControllerReceiversInCorestartable); + FeatureFlags::registerBatteryControllerReceiversInCorestartable); } @Override + + public boolean registerContentObserversAsync() { + return getValue(Flags.FLAG_REGISTER_CONTENT_OBSERVERS_ASYNC, + FeatureFlags::registerContentObserversAsync); + } + + @Override + public boolean registerNewWalletCardInBackground() { return getValue(Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, - FeatureFlags::registerNewWalletCardInBackground); + FeatureFlags::registerNewWalletCardInBackground); } @Override + public boolean registerWallpaperNotifierBackground() { return getValue(Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, - FeatureFlags::registerWallpaperNotifierBackground); + FeatureFlags::registerWallpaperNotifierBackground); } @Override - public boolean registerZenModeContentObserverBackground() { - return getValue(Flags.FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND, - FeatureFlags::registerZenModeContentObserverBackground); + + public boolean relockWithPowerButtonImmediately() { + return getValue(Flags.FLAG_RELOCK_WITH_POWER_BUTTON_IMMEDIATELY, + FeatureFlags::relockWithPowerButtonImmediately); } @Override + public boolean removeDreamOverlayHideOnTouch() { return getValue(Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, - FeatureFlags::removeDreamOverlayHideOnTouch); + FeatureFlags::removeDreamOverlayHideOnTouch); } @Override + + public boolean removeUpdateListenerInQsIconViewImpl() { + return getValue(Flags.FLAG_REMOVE_UPDATE_LISTENER_IN_QS_ICON_VIEW_IMPL, + FeatureFlags::removeUpdateListenerInQsIconViewImpl); + } + + @Override + public boolean restToUnlock() { return getValue(Flags.FLAG_REST_TO_UNLOCK, - FeatureFlags::restToUnlock); + FeatureFlags::restToUnlock); } @Override + public boolean restartDreamOnUnocclude() { return getValue(Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, - FeatureFlags::restartDreamOnUnocclude); + FeatureFlags::restartDreamOnUnocclude); } @Override + public boolean revampedBouncerMessages() { return getValue(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, - FeatureFlags::revampedBouncerMessages); + FeatureFlags::revampedBouncerMessages); } @Override + public boolean runFingerprintDetectOnDismissibleKeyguard() { return getValue(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, - FeatureFlags::runFingerprintDetectOnDismissibleKeyguard); + FeatureFlags::runFingerprintDetectOnDismissibleKeyguard); } @Override + public boolean saveAndRestoreMagnificationSettingsButtons() { return getValue(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, - FeatureFlags::saveAndRestoreMagnificationSettingsButtons); + FeatureFlags::saveAndRestoreMagnificationSettingsButtons); } @Override + public boolean sceneContainer() { return getValue(Flags.FLAG_SCENE_CONTAINER, - FeatureFlags::sceneContainer); + FeatureFlags::sceneContainer); } @Override + public boolean screenshareNotificationHidingBugFix() { return getValue(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, - FeatureFlags::screenshareNotificationHidingBugFix); + FeatureFlags::screenshareNotificationHidingBugFix); } @Override + public boolean screenshotActionDismissSystemWindows() { return getValue(Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, - FeatureFlags::screenshotActionDismissSystemWindows); + FeatureFlags::screenshotActionDismissSystemWindows); } @Override - public boolean screenshotPrivateProfileAccessibilityAnnouncementFix() { - return getValue(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX, - FeatureFlags::screenshotPrivateProfileAccessibilityAnnouncementFix); + + public boolean screenshotMultidisplayFocusChange() { + return getValue(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE, + FeatureFlags::screenshotMultidisplayFocusChange); } @Override - public boolean screenshotPrivateProfileBehaviorFix() { - return getValue(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX, - FeatureFlags::screenshotPrivateProfileBehaviorFix); + + public boolean screenshotPolicySplitAndDesktopMode() { + return getValue(Flags.FLAG_SCREENSHOT_POLICY_SPLIT_AND_DESKTOP_MODE, + FeatureFlags::screenshotPolicySplitAndDesktopMode); } @Override + public boolean screenshotScrollCropViewCrashFix() { return getValue(Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, - FeatureFlags::screenshotScrollCropViewCrashFix); + FeatureFlags::screenshotScrollCropViewCrashFix); } @Override - public boolean screenshotShelfUi2() { - return getValue(Flags.FLAG_SCREENSHOT_SHELF_UI2, - FeatureFlags::screenshotShelfUi2); + + public boolean screenshotUiControllerRefactor() { + return getValue(Flags.FLAG_SCREENSHOT_UI_CONTROLLER_REFACTOR, + FeatureFlags::screenshotUiControllerRefactor); } @Override - public boolean shadeCollapseActivityLaunchFix() { - return getValue(Flags.FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX, - FeatureFlags::shadeCollapseActivityLaunchFix); + + public boolean secondaryUserWidgetHost() { + return getValue(Flags.FLAG_SECONDARY_USER_WIDGET_HOST, + FeatureFlags::secondaryUserWidgetHost); } @Override + + public boolean settingsExtRegisterContentObserverOnBgThread() { + return getValue(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD, + FeatureFlags::settingsExtRegisterContentObserverOnBgThread); + } + + @Override + + public boolean shadeExpandsOnStatusBarLongPress() { + return getValue(Flags.FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS, + FeatureFlags::shadeExpandsOnStatusBarLongPress); + } + + @Override + + public boolean shadeHeaderFontUpdate() { + return getValue(Flags.FLAG_SHADE_HEADER_FONT_UPDATE, + FeatureFlags::shadeHeaderFontUpdate); + } + + @Override + + public boolean shadeLaunchAccessibility() { + return getValue(Flags.FLAG_SHADE_LAUNCH_ACCESSIBILITY, + FeatureFlags::shadeLaunchAccessibility); + } + + @Override + + public boolean shadeWindowGoesAround() { + return getValue(Flags.FLAG_SHADE_WINDOW_GOES_AROUND, + FeatureFlags::shadeWindowGoesAround); + } + + @Override + public boolean shaderlibLoadingEffectRefactor() { return getValue(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, - FeatureFlags::shaderlibLoadingEffectRefactor); + FeatureFlags::shaderlibLoadingEffectRefactor); } @Override + + public boolean shortcutHelperKeyGlyph() { + return getValue(Flags.FLAG_SHORTCUT_HELPER_KEY_GLYPH, + FeatureFlags::shortcutHelperKeyGlyph); + } + + @Override + + public boolean showAudioSharingSliderInVolumePanel() { + return getValue(Flags.FLAG_SHOW_AUDIO_SHARING_SLIDER_IN_VOLUME_PANEL, + FeatureFlags::showAudioSharingSliderInVolumePanel); + } + + @Override + + public boolean showClipboardIndication() { + return getValue(Flags.FLAG_SHOW_CLIPBOARD_INDICATION, + FeatureFlags::showClipboardIndication); + } + + @Override + + public boolean showLockedByYourWatchKeyguardIndicator() { + return getValue(Flags.FLAG_SHOW_LOCKED_BY_YOUR_WATCH_KEYGUARD_INDICATOR, + FeatureFlags::showLockedByYourWatchKeyguardIndicator); + } + + @Override + + public boolean showToastWhenAppControlBrightness() { + return getValue(Flags.FLAG_SHOW_TOAST_WHEN_APP_CONTROL_BRIGHTNESS, + FeatureFlags::showToastWhenAppControlBrightness); + } + + @Override + + public boolean simPinBouncerReset() { + return getValue(Flags.FLAG_SIM_PIN_BOUNCER_RESET, + FeatureFlags::simPinBouncerReset); + } + + @Override + + public boolean simPinRaceConditionOnRestart() { + return getValue(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART, + FeatureFlags::simPinRaceConditionOnRestart); + } + + @Override + + public boolean simPinUseSlotId() { + return getValue(Flags.FLAG_SIM_PIN_USE_SLOT_ID, + FeatureFlags::simPinUseSlotId); + } + + @Override + + public boolean skipHideSensitiveNotifAnimation() { + return getValue(Flags.FLAG_SKIP_HIDE_SENSITIVE_NOTIF_ANIMATION, + FeatureFlags::skipHideSensitiveNotifAnimation); + } + + @Override + public boolean sliceBroadcastRelayInBackground() { return getValue(Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, - FeatureFlags::sliceBroadcastRelayInBackground); + FeatureFlags::sliceBroadcastRelayInBackground); } @Override + public boolean sliceManagerBinderCallBackground() { return getValue(Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, - FeatureFlags::sliceManagerBinderCallBackground); + FeatureFlags::sliceManagerBinderCallBackground); } @Override + public boolean smartspaceLockscreenViewmodel() { return getValue(Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, - FeatureFlags::smartspaceLockscreenViewmodel); + FeatureFlags::smartspaceLockscreenViewmodel); } @Override + public boolean smartspaceRelocateToBottom() { return getValue(Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, - FeatureFlags::smartspaceRelocateToBottom); + FeatureFlags::smartspaceRelocateToBottom); } @Override - public boolean smartspaceRemoteviewsRendering() { - return getValue(Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING, - FeatureFlags::smartspaceRemoteviewsRendering); + + public boolean smartspaceRemoteviewsRenderingFix() { + return getValue(Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING_FIX, + FeatureFlags::smartspaceRemoteviewsRenderingFix); } @Override + + public boolean smartspaceSwipeEventLoggingFix() { + return getValue(Flags.FLAG_SMARTSPACE_SWIPE_EVENT_LOGGING_FIX, + FeatureFlags::smartspaceSwipeEventLoggingFix); + } + + @Override + + public boolean smartspaceViewpager2() { + return getValue(Flags.FLAG_SMARTSPACE_VIEWPAGER2, + FeatureFlags::smartspaceViewpager2); + } + + @Override + + public boolean sounddoseCustomization() { + return getValue(Flags.FLAG_SOUNDDOSE_CUSTOMIZATION, + FeatureFlags::sounddoseCustomization); + } + + @Override + + public boolean spatialModelAppPushback() { + return getValue(Flags.FLAG_SPATIAL_MODEL_APP_PUSHBACK, + FeatureFlags::spatialModelAppPushback); + } + + @Override + + public boolean stabilizeHeadsUpGroupV2() { + return getValue(Flags.FLAG_STABILIZE_HEADS_UP_GROUP_V2, + FeatureFlags::stabilizeHeadsUpGroupV2); + } + + @Override + + public boolean statusBarAlwaysCheckUnderlyingNetworks() { + return getValue(Flags.FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS, + FeatureFlags::statusBarAlwaysCheckUnderlyingNetworks); + } + + @Override + + public boolean statusBarAutoStartScreenRecordChip() { + return getValue(Flags.FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP, + FeatureFlags::statusBarAutoStartScreenRecordChip); + } + + @Override + + public boolean statusBarChipsModernization() { + return getValue(Flags.FLAG_STATUS_BAR_CHIPS_MODERNIZATION, + FeatureFlags::statusBarChipsModernization); + } + + @Override + + public boolean statusBarChipsReturnAnimations() { + return getValue(Flags.FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS, + FeatureFlags::statusBarChipsReturnAnimations); + } + + @Override + + public boolean statusBarFontUpdates() { + return getValue(Flags.FLAG_STATUS_BAR_FONT_UPDATES, + FeatureFlags::statusBarFontUpdates); + } + + @Override + + public boolean statusBarMobileIconKairos() { + return getValue(Flags.FLAG_STATUS_BAR_MOBILE_ICON_KAIROS, + FeatureFlags::statusBarMobileIconKairos); + } + + @Override + public boolean statusBarMonochromeIconsFix() { return getValue(Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, - FeatureFlags::statusBarMonochromeIconsFix); + FeatureFlags::statusBarMonochromeIconsFix); } @Override - public boolean statusBarScreenSharingChips() { - return getValue(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - FeatureFlags::statusBarScreenSharingChips); + + public boolean statusBarNoHunBehavior() { + return getValue(Flags.FLAG_STATUS_BAR_NO_HUN_BEHAVIOR, + FeatureFlags::statusBarNoHunBehavior); } @Override + + public boolean statusBarPopupChips() { + return getValue(Flags.FLAG_STATUS_BAR_POPUP_CHIPS, + FeatureFlags::statusBarPopupChips); + } + + @Override + + public boolean statusBarRootModernization() { + return getValue(Flags.FLAG_STATUS_BAR_ROOT_MODERNIZATION, + FeatureFlags::statusBarRootModernization); + } + + @Override + + public boolean statusBarShowAudioOnlyProjectionChip() { + return getValue(Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP, + FeatureFlags::statusBarShowAudioOnlyProjectionChip); + } + + @Override + + public boolean statusBarSignalPolicyRefactor() { + return getValue(Flags.FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR, + FeatureFlags::statusBarSignalPolicyRefactor); + } + + @Override + + public boolean statusBarSignalPolicyRefactorEthernet() { + return getValue(Flags.FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR_ETHERNET, + FeatureFlags::statusBarSignalPolicyRefactorEthernet); + } + + @Override + public boolean statusBarStaticInoutIndicators() { return getValue(Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, - FeatureFlags::statusBarStaticInoutIndicators); + FeatureFlags::statusBarStaticInoutIndicators); } @Override + + public boolean statusBarStopUpdatingWindowHeight() { + return getValue(Flags.FLAG_STATUS_BAR_STOP_UPDATING_WINDOW_HEIGHT, + FeatureFlags::statusBarStopUpdatingWindowHeight); + } + + @Override + + public boolean statusBarSwipeOverChip() { + return getValue(Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP, + FeatureFlags::statusBarSwipeOverChip); + } + + @Override + + public boolean statusBarSwitchToSpnFromDataSpn() { + return getValue(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN, + FeatureFlags::statusBarSwitchToSpnFromDataSpn); + } + + @Override + + public boolean statusBarUiThread() { + return getValue(Flags.FLAG_STATUS_BAR_UI_THREAD, + FeatureFlags::statusBarUiThread); + } + + @Override + + public boolean statusBarWindowNoCustomTouch() { + return getValue(Flags.FLAG_STATUS_BAR_WINDOW_NO_CUSTOM_TOUCH, + FeatureFlags::statusBarWindowNoCustomTouch); + } + + @Override + + public boolean stoppableFgsSystemApp() { + return getValue(Flags.FLAG_STOPPABLE_FGS_SYSTEM_APP, + FeatureFlags::stoppableFgsSystemApp); + } + + @Override + public boolean switchUserOnBg() { return getValue(Flags.FLAG_SWITCH_USER_ON_BG, - FeatureFlags::switchUserOnBg); + FeatureFlags::switchUserOnBg); } @Override + public boolean sysuiTeamfood() { return getValue(Flags.FLAG_SYSUI_TEAMFOOD, - FeatureFlags::sysuiTeamfood); + FeatureFlags::sysuiTeamfood); } @Override + public boolean themeOverlayControllerWakefulnessDeprecation() { return getValue(Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, - FeatureFlags::themeOverlayControllerWakefulnessDeprecation); + FeatureFlags::themeOverlayControllerWakefulnessDeprecation); } @Override + + public boolean transitionRaceCondition() { + return getValue(Flags.FLAG_TRANSITION_RACE_CONDITION, + FeatureFlags::transitionRaceCondition); + } + + @Override + public boolean translucentOccludingActivityFix() { return getValue(Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, - FeatureFlags::translucentOccludingActivityFix); + FeatureFlags::translucentOccludingActivityFix); } @Override - public boolean truncatedStatusBarIconsFix() { - return getValue(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX, - FeatureFlags::truncatedStatusBarIconsFix); + + public boolean tvGlobalActionsFocus() { + return getValue(Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS, + FeatureFlags::tvGlobalActionsFocus); } @Override + public boolean udfpsViewPerformance() { return getValue(Flags.FLAG_UDFPS_VIEW_PERFORMANCE, - FeatureFlags::udfpsViewPerformance); + FeatureFlags::udfpsViewPerformance); } @Override + public boolean unfoldAnimationBackgroundProgress() { return getValue(Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, - FeatureFlags::unfoldAnimationBackgroundProgress); + FeatureFlags::unfoldAnimationBackgroundProgress); } @Override + + public boolean unfoldLatencyTrackingFix() { + return getValue(Flags.FLAG_UNFOLD_LATENCY_TRACKING_FIX, + FeatureFlags::unfoldLatencyTrackingFix); + } + + @Override + + public boolean updateCornerRadiusOnDisplayChanged() { + return getValue(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED, + FeatureFlags::updateCornerRadiusOnDisplayChanged); + } + + @Override + public boolean updateUserSwitcherBackground() { return getValue(Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, - FeatureFlags::updateUserSwitcherBackground); + FeatureFlags::updateUserSwitcherBackground); } @Override - public boolean validateKeyboardShortcutHelperIconUri() { - return getValue(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI, - FeatureFlags::validateKeyboardShortcutHelperIconUri); + + public boolean updateWindowMagnifierBottomBoundary() { + return getValue(Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY, + FeatureFlags::updateWindowMagnifierBottomBoundary); } @Override + + public boolean useAadProxSensor() { + return getValue(Flags.FLAG_USE_AAD_PROX_SENSOR, + FeatureFlags::useAadProxSensor); + } + + @Override + + public boolean useNotifInflationThreadForFooter() { + return getValue(Flags.FLAG_USE_NOTIF_INFLATION_THREAD_FOR_FOOTER, + FeatureFlags::useNotifInflationThreadForFooter); + } + + @Override + + public boolean useNotifInflationThreadForRow() { + return getValue(Flags.FLAG_USE_NOTIF_INFLATION_THREAD_FOR_ROW, + FeatureFlags::useNotifInflationThreadForRow); + } + + @Override + + public boolean useTransitionsForKeyguardOccluded() { + return getValue(Flags.FLAG_USE_TRANSITIONS_FOR_KEYGUARD_OCCLUDED, + FeatureFlags::useTransitionsForKeyguardOccluded); + } + + @Override + + public boolean useVolumeController() { + return getValue(Flags.FLAG_USE_VOLUME_CONTROLLER, + FeatureFlags::useVolumeController); + } + + @Override + + public boolean userAwareSettingsRepositories() { + return getValue(Flags.FLAG_USER_AWARE_SETTINGS_REPOSITORIES, + FeatureFlags::userAwareSettingsRepositories); + } + + @Override + + public boolean userEncryptedSource() { + return getValue(Flags.FLAG_USER_ENCRYPTED_SOURCE, + FeatureFlags::userEncryptedSource); + } + + @Override + + public boolean userSwitcherAddSignOutOption() { + return getValue(Flags.FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION, + FeatureFlags::userSwitcherAddSignOutOption); + } + + @Override + public boolean visualInterruptionsRefactor() { return getValue(Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR, - FeatureFlags::visualInterruptionsRefactor); + FeatureFlags::visualInterruptionsRefactor); + } + + @Override + + public boolean volumeRedesign() { + return getValue(Flags.FLAG_VOLUME_REDESIGN, + FeatureFlags::volumeRedesign); } public boolean isFlagReadOnlyOptimized(String flagName) { if (mReadOnlyFlagsSet.contains(flagName) && - isOptimizationEnabled()) { - return true; + isOptimizationEnabled()) { + return true; } return false; } + private boolean isOptimizationEnabled() { return false; } @@ -946,163 +1994,572 @@ public class CustomFeatureFlags implements FeatureFlags { public List getFlagNames() { return Arrays.asList( - Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, - Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, - Flags.FLAG_APP_CLIPS_BACKLINKS, - Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, - Flags.FLAG_BP_TALKBACK, - Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, - Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX, - Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, - Flags.FLAG_CLOCK_REACTIVE_VARIANTS, - Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, - Flags.FLAG_COMMUNAL_HUB, - Flags.FLAG_COMPOSE_BOUNCER, - Flags.FLAG_COMPOSE_LOCKSCREEN, - Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, - Flags.FLAG_CONSTRAINT_BP, - Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, - Flags.FLAG_COROUTINE_TRACING, - Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, - Flags.FLAG_DEDICATED_NOTIF_INFLATION_THREAD, - Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, - Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD, - Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, - Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, - Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, - Flags.FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION, - Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, - Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, - Flags.FLAG_DUAL_SHADE, - Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, - Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, - Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, - Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, - Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, - Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, - Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, - Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, - Flags.FLAG_ENABLE_LAYOUT_TRACING, - Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, - Flags.FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER, - Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, - Flags.FLAG_EXAMPLE_FLAG, - Flags.FLAG_FAST_UNLOCK_TRANSITION, - Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, - Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, - Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, - Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, - Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, - Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, - Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, - Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, - Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, - Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, - Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, - Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE, - Flags.FLAG_GLANCEABLE_HUB_GESTURE_HANDLE, - Flags.FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON, - Flags.FLAG_HAPTIC_BRIGHTNESS_SLIDER, - Flags.FLAG_HAPTIC_VOLUME_SLIDER, - Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, - Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, - Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, - Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, - Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, - Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, - Flags.FLAG_LIGHT_REVEAL_MIGRATION, - Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, - Flags.FLAG_MEDIA_CONTROLS_REFACTOR, - Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, - Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - Flags.FLAG_NEW_AOD_TRANSITION, - Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, - Flags.FLAG_NEW_VOLUME_PANEL, - Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, - Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, - Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, - Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, - Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, - Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, - Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, - Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, - Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION, - Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE, - Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, - Flags.FLAG_NOTIFICATION_PULSING_FIX, - Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, - Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, - Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, - Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, - Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR, - Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR, - Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, - Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, - Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION, - Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, - Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, - Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, - Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER, - Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS, - Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, - Flags.FLAG_PREDICTIVE_BACK_SYSUI, - Flags.FLAG_PRIORITY_PEOPLE_SECTION, - Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX, - Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX, - Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, - Flags.FLAG_PSS_TASK_SWITCHER, - Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, - Flags.FLAG_QS_NEW_PIPELINE, - Flags.FLAG_QS_NEW_TILES, - Flags.FLAG_QS_NEW_TILES_FUTURE, - Flags.FLAG_QS_TILE_FOCUS_STATE, - Flags.FLAG_QS_UI_REFACTOR, - Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS, - Flags.FLAG_RECORD_ISSUE_QS_TILE, - Flags.FLAG_REFACTOR_GET_CURRENT_USER, - Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, - Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, - Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, - Flags.FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND, - Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, - Flags.FLAG_REST_TO_UNLOCK, - Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, - Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, - Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, - Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, - Flags.FLAG_SCENE_CONTAINER, - Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, - Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, - Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX, - Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX, - Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, - Flags.FLAG_SCREENSHOT_SHELF_UI2, - Flags.FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX, - Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, - Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, - Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, - Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, - Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, - Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING, - Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, - Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, - Flags.FLAG_SWITCH_USER_ON_BG, - Flags.FLAG_SYSUI_TEAMFOOD, - Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, - Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, - Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX, - Flags.FLAG_UDFPS_VIEW_PERFORMANCE, - Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, - Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, - Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI, - Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR + Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, + Flags.FLAG_ADD_BLACK_BACKGROUND_FOR_WINDOW_MAGNIFIER, + Flags.FLAG_ALWAYS_COMPOSE_QS_UI_FRAGMENT, + Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, + Flags.FLAG_APP_CLIPS_BACKLINKS, + Flags.FLAG_APP_SHORTCUT_REMOVAL_FIX, + Flags.FLAG_AVALANCHE_REPLACE_HUN_WHEN_CRITICAL, + Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, + Flags.FLAG_BOUNCER_UI_REVAMP, + Flags.FLAG_BOUNCER_UI_REVAMP_2, + Flags.FLAG_BP_COLORS, + Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, + Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION, + Flags.FLAG_CLASSIC_FLAGS_MULTI_USER, + Flags.FLAG_CLIPBOARD_IMAGE_TIMEOUT, + Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, + Flags.FLAG_CLIPBOARD_OVERLAY_MULTIUSER, + Flags.FLAG_CLIPBOARD_SHARED_TRANSITIONS, + Flags.FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE, + Flags.FLAG_CLOCK_FIDGET_ANIMATION, + Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, + Flags.FLAG_COMMUNAL_EDIT_WIDGETS_ACTIVITY_FINISH_FIX, + Flags.FLAG_COMMUNAL_HUB, + Flags.FLAG_COMMUNAL_HUB_USE_THREAD_POOL_FOR_WIDGETS, + Flags.FLAG_COMMUNAL_RESPONSIVE_GRID, + Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + Flags.FLAG_COMMUNAL_STANDALONE_SUPPORT, + Flags.FLAG_COMMUNAL_TIMER_FLICKER_FIX, + Flags.FLAG_COMMUNAL_WIDGET_RESIZING, + Flags.FLAG_COMMUNAL_WIDGET_TRAMPOLINE_FIX, + Flags.FLAG_COMPOSE_BOUNCER, + Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, + Flags.FLAG_CONT_AUTH_PLUGIN, + Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, + Flags.FLAG_COROUTINE_TRACING, + Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, + Flags.FLAG_DEBUG_LIVE_UPDATES_PROMOTE_ALL, + Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB, + Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, + Flags.FLAG_DESKTOP_EFFECTS_QS_TILE, + Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, + Flags.FLAG_DISABLE_BLURRED_SHADE_VISIBLE, + Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, + Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, + Flags.FLAG_DISABLE_SHADE_TRACKPAD_TWO_FINGER_SWIPE, + Flags.FLAG_DOUBLE_TAP_TO_SLEEP, + Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, + Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, + Flags.FLAG_DREAM_OVERLAY_UPDATED_FONT, + Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, + Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, + Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, + Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, + Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, + Flags.FLAG_ENABLE_LAYOUT_TRACING, + Flags.FLAG_ENABLE_UNDERLAY, + Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, + Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, + Flags.FLAG_EXAMPLE_FLAG, + Flags.FLAG_EXPAND_COLLAPSE_PRIVACY_DIALOG, + Flags.FLAG_EXPAND_HEADS_UP_ON_INLINE_REPLY, + Flags.FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN, + Flags.FLAG_EXTENDED_APPS_SHORTCUT_CATEGORY, + Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE, + Flags.FLAG_FACE_SCANNING_ANIMATION_NPE_FIX, + Flags.FLAG_FASTER_UNLOCK_TRANSITION, + Flags.FLAG_FETCH_BOOKMARKS_XML_KEYBOARD_SHORTCUTS, + Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, + Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, + Flags.FLAG_FLOATING_MENU_DISPLAY_CUTOUT_SUPPORT, + Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, + Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, + Flags.FLAG_FLOATING_MENU_HEARING_DEVICE_STATUS_ICON, + Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, + Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, + Flags.FLAG_FLOATING_MENU_NOTIFY_TARGETS_CHANGED_ON_STRICT_DIFF, + Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, + Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, + Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, + Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, + Flags.FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND, + Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE, + Flags.FLAG_GLANCEABLE_HUB_V2, + Flags.FLAG_GLANCEABLE_HUB_V2_RESOURCES, + Flags.FLAG_HAPTICS_FOR_COMPOSE_SLIDERS, + Flags.FLAG_HARDWARE_COLOR_STYLES, + Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, + Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, + Flags.FLAG_HIDE_RINGER_BUTTON_IN_SINGLE_VOLUME_MODE, + Flags.FLAG_HOME_CONTROLS_DREAM_HSUM, + Flags.FLAG_HUB_EDIT_MODE_TOUCH_ADJUSTMENTS, + Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE, + Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, + Flags.FLAG_ICON_REFRESH_2025, + Flags.FLAG_IGNORE_TOUCHES_NEXT_TO_NOTIFICATION_SHELF, + Flags.FLAG_INDICATION_TEXT_A11Y_FIX, + Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, + Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, + Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_SHORTCUT_CUSTOMIZER, + Flags.FLAG_KEYBOARD_TOUCHPAD_CONTEXTUAL_EDUCATION, + Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF, + Flags.FLAG_KEYGUARD_WM_REORDER_ATMS_CALLS, + Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + Flags.FLAG_LOCKSCREEN_FONT, + Flags.FLAG_LOW_LIGHT_CLOCK_DREAM, + Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES, + Flags.FLAG_MEDIA_CONTROLS_A11Y_COLORS, + Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3, + Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3_PLACEMENT, + Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION, + Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE_BUGFIX, + Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, + Flags.FLAG_MEDIA_CONTROLS_UI_UPDATE, + Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND, + Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, + Flags.FLAG_MEDIA_LOAD_METADATA_VIA_MEDIA_DATA_LOADER, + Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION, + Flags.FLAG_MEDIA_PROJECTION_DIALOG_BEHIND_LOCKSCREEN, + Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT, + Flags.FLAG_MEDIA_PROJECTION_REQUEST_ATTRIBUTION_FIX, + Flags.FLAG_MODES_UI_DIALOG_PAGING, + Flags.FLAG_MOVE_TRANSITION_ANIMATION_LAYER, + Flags.FLAG_MSDL_FEEDBACK, + Flags.FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT, + Flags.FLAG_NEW_AOD_TRANSITION, + Flags.FLAG_NEW_VOLUME_PANEL, + Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING, + Flags.FLAG_NOTES_ROLE_QS_TILE, + Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS, + Flags.FLAG_NOTIFICATION_AMBIENT_SUPPRESSION_AFTER_INFLATION, + Flags.FLAG_NOTIFICATION_ANIMATED_ACTIONS_TREATMENT, + Flags.FLAG_NOTIFICATION_APPEAR_NONLINEAR, + Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, + Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, + Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, + Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, + Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_BUNDLE_UI, + Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, + Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, + Flags.FLAG_NOTIFICATION_REENTRANT_DISMISS, + Flags.FLAG_NOTIFICATION_ROW_ACCESSIBILITY_EXPANDED, + Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, + Flags.FLAG_NOTIFICATION_ROW_TRANSPARENCY, + Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, + Flags.FLAG_NOTIFICATION_SHADE_BLUR, + Flags.FLAG_NOTIFICATION_SHADE_UI_THREAD, + Flags.FLAG_NOTIFICATION_SKIP_SILENT_UPDATES, + Flags.FLAG_NOTIFICATION_TRANSPARENT_HEADER_FIX, + Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, + Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, + Flags.FLAG_NOTIFICATIONS_FOOTER_VISIBILITY_FIX, + Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, + Flags.FLAG_NOTIFICATIONS_HUN_SHARED_ANIMATION_VALUES, + Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, + Flags.FLAG_NOTIFICATIONS_LAUNCH_RADIUS, + Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, + Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE, + Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW, + Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS, + Flags.FLAG_NOTIFY_PASSWORD_TEXT_VIEW_USER_ACTIVITY_IN_BACKGROUND, + Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, + Flags.FLAG_ONLY_SHOW_MEDIA_STREAM_SLIDER_IN_SINGLE_VOLUME_MODE, + Flags.FLAG_OUTPUT_SWITCHER_REDESIGN, + Flags.FLAG_OVERRIDE_SUPPRESS_OVERLAY_CONDITION, + Flags.FLAG_PERMISSION_HELPER_INLINE_UI_RICH_ONGOING, + Flags.FLAG_PERMISSION_HELPER_UI_RICH_ONGOING, + Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT, + Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, + Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, + Flags.FLAG_PREDICTIVE_BACK_DELAY_WM_TRANSITION, + Flags.FLAG_PRIORITY_PEOPLE_SECTION, + Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, + Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, + Flags.FLAG_PSS_TASK_SWITCHER, + Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + Flags.FLAG_QS_NEW_TILES, + Flags.FLAG_QS_NEW_TILES_FUTURE, + Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES, + Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD, + Flags.FLAG_QS_TILE_DETAILED_VIEW, + Flags.FLAG_QS_TILE_FOCUS_STATE, + Flags.FLAG_QS_UI_REFACTOR, + Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT, + Flags.FLAG_RECORD_ISSUE_QS_TILE, + Flags.FLAG_REDESIGN_MAGNIFICATION_WINDOW_SIZE, + Flags.FLAG_REFACTOR_GET_CURRENT_USER, + Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, + Flags.FLAG_REGISTER_CONTENT_OBSERVERS_ASYNC, + Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, + Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, + Flags.FLAG_RELOCK_WITH_POWER_BUTTON_IMMEDIATELY, + Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, + Flags.FLAG_REMOVE_UPDATE_LISTENER_IN_QS_ICON_VIEW_IMPL, + Flags.FLAG_REST_TO_UNLOCK, + Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, + Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, + Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, + Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, + Flags.FLAG_SCENE_CONTAINER, + Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, + Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE, + Flags.FLAG_SCREENSHOT_POLICY_SPLIT_AND_DESKTOP_MODE, + Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, + Flags.FLAG_SCREENSHOT_UI_CONTROLLER_REFACTOR, + Flags.FLAG_SECONDARY_USER_WIDGET_HOST, + Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD, + Flags.FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS, + Flags.FLAG_SHADE_HEADER_FONT_UPDATE, + Flags.FLAG_SHADE_LAUNCH_ACCESSIBILITY, + Flags.FLAG_SHADE_WINDOW_GOES_AROUND, + Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, + Flags.FLAG_SHORTCUT_HELPER_KEY_GLYPH, + Flags.FLAG_SHOW_AUDIO_SHARING_SLIDER_IN_VOLUME_PANEL, + Flags.FLAG_SHOW_CLIPBOARD_INDICATION, + Flags.FLAG_SHOW_LOCKED_BY_YOUR_WATCH_KEYGUARD_INDICATOR, + Flags.FLAG_SHOW_TOAST_WHEN_APP_CONTROL_BRIGHTNESS, + Flags.FLAG_SIM_PIN_BOUNCER_RESET, + Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART, + Flags.FLAG_SIM_PIN_USE_SLOT_ID, + Flags.FLAG_SKIP_HIDE_SENSITIVE_NOTIF_ANIMATION, + Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, + Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, + Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, + Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, + Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING_FIX, + Flags.FLAG_SMARTSPACE_SWIPE_EVENT_LOGGING_FIX, + Flags.FLAG_SMARTSPACE_VIEWPAGER2, + Flags.FLAG_SOUNDDOSE_CUSTOMIZATION, + Flags.FLAG_SPATIAL_MODEL_APP_PUSHBACK, + Flags.FLAG_STABILIZE_HEADS_UP_GROUP_V2, + Flags.FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS, + Flags.FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP, + Flags.FLAG_STATUS_BAR_CHIPS_MODERNIZATION, + Flags.FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS, + Flags.FLAG_STATUS_BAR_FONT_UPDATES, + Flags.FLAG_STATUS_BAR_MOBILE_ICON_KAIROS, + Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, + Flags.FLAG_STATUS_BAR_NO_HUN_BEHAVIOR, + Flags.FLAG_STATUS_BAR_POPUP_CHIPS, + Flags.FLAG_STATUS_BAR_ROOT_MODERNIZATION, + Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP, + Flags.FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR, + Flags.FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR_ETHERNET, + Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, + Flags.FLAG_STATUS_BAR_STOP_UPDATING_WINDOW_HEIGHT, + Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP, + Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN, + Flags.FLAG_STATUS_BAR_UI_THREAD, + Flags.FLAG_STATUS_BAR_WINDOW_NO_CUSTOM_TOUCH, + Flags.FLAG_STOPPABLE_FGS_SYSTEM_APP, + Flags.FLAG_SWITCH_USER_ON_BG, + Flags.FLAG_SYSUI_TEAMFOOD, + Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, + Flags.FLAG_TRANSITION_RACE_CONDITION, + Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, + Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS, + Flags.FLAG_UDFPS_VIEW_PERFORMANCE, + Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, + Flags.FLAG_UNFOLD_LATENCY_TRACKING_FIX, + Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED, + Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, + Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY, + Flags.FLAG_USE_AAD_PROX_SENSOR, + Flags.FLAG_USE_NOTIF_INFLATION_THREAD_FOR_FOOTER, + Flags.FLAG_USE_NOTIF_INFLATION_THREAD_FOR_ROW, + Flags.FLAG_USE_TRANSITIONS_FOR_KEYGUARD_OCCLUDED, + Flags.FLAG_USE_VOLUME_CONTROLLER, + Flags.FLAG_USER_AWARE_SETTINGS_REPOSITORIES, + Flags.FLAG_USER_ENCRYPTED_SOURCE, + Flags.FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION, + Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR, + Flags.FLAG_VOLUME_REDESIGN ); } private Set mReadOnlyFlagsSet = new HashSet<>( - Arrays.asList( - "" - ) + Arrays.asList( + Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, + Flags.FLAG_ADD_BLACK_BACKGROUND_FOR_WINDOW_MAGNIFIER, + Flags.FLAG_ALWAYS_COMPOSE_QS_UI_FRAGMENT, + Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, + Flags.FLAG_APP_CLIPS_BACKLINKS, + Flags.FLAG_APP_SHORTCUT_REMOVAL_FIX, + Flags.FLAG_AVALANCHE_REPLACE_HUN_WHEN_CRITICAL, + Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, + Flags.FLAG_BOUNCER_UI_REVAMP, + Flags.FLAG_BOUNCER_UI_REVAMP_2, + Flags.FLAG_BP_COLORS, + Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, + Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION, + Flags.FLAG_CLASSIC_FLAGS_MULTI_USER, + Flags.FLAG_CLIPBOARD_IMAGE_TIMEOUT, + Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, + Flags.FLAG_CLIPBOARD_OVERLAY_MULTIUSER, + Flags.FLAG_CLIPBOARD_SHARED_TRANSITIONS, + Flags.FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE, + Flags.FLAG_CLOCK_FIDGET_ANIMATION, + Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, + Flags.FLAG_COMMUNAL_EDIT_WIDGETS_ACTIVITY_FINISH_FIX, + Flags.FLAG_COMMUNAL_HUB, + Flags.FLAG_COMMUNAL_HUB_USE_THREAD_POOL_FOR_WIDGETS, + Flags.FLAG_COMMUNAL_RESPONSIVE_GRID, + Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + Flags.FLAG_COMMUNAL_STANDALONE_SUPPORT, + Flags.FLAG_COMMUNAL_TIMER_FLICKER_FIX, + Flags.FLAG_COMMUNAL_WIDGET_RESIZING, + Flags.FLAG_COMMUNAL_WIDGET_TRAMPOLINE_FIX, + Flags.FLAG_COMPOSE_BOUNCER, + Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, + Flags.FLAG_CONT_AUTH_PLUGIN, + Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, + Flags.FLAG_COROUTINE_TRACING, + Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, + Flags.FLAG_DEBUG_LIVE_UPDATES_PROMOTE_ALL, + Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB, + Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, + Flags.FLAG_DESKTOP_EFFECTS_QS_TILE, + Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, + Flags.FLAG_DISABLE_BLURRED_SHADE_VISIBLE, + Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, + Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, + Flags.FLAG_DISABLE_SHADE_TRACKPAD_TWO_FINGER_SWIPE, + Flags.FLAG_DOUBLE_TAP_TO_SLEEP, + Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, + Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, + Flags.FLAG_DREAM_OVERLAY_UPDATED_FONT, + Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, + Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, + Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, + Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, + Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, + Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, + Flags.FLAG_ENABLE_LAYOUT_TRACING, + Flags.FLAG_ENABLE_UNDERLAY, + Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, + Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, + Flags.FLAG_EXAMPLE_FLAG, + Flags.FLAG_EXPAND_COLLAPSE_PRIVACY_DIALOG, + Flags.FLAG_EXPAND_HEADS_UP_ON_INLINE_REPLY, + Flags.FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN, + Flags.FLAG_EXTENDED_APPS_SHORTCUT_CATEGORY, + Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE, + Flags.FLAG_FACE_SCANNING_ANIMATION_NPE_FIX, + Flags.FLAG_FASTER_UNLOCK_TRANSITION, + Flags.FLAG_FETCH_BOOKMARKS_XML_KEYBOARD_SHORTCUTS, + Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, + Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, + Flags.FLAG_FLOATING_MENU_DISPLAY_CUTOUT_SUPPORT, + Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, + Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, + Flags.FLAG_FLOATING_MENU_HEARING_DEVICE_STATUS_ICON, + Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, + Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, + Flags.FLAG_FLOATING_MENU_NOTIFY_TARGETS_CHANGED_ON_STRICT_DIFF, + Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, + Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, + Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, + Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, + Flags.FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND, + Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE, + Flags.FLAG_GLANCEABLE_HUB_V2, + Flags.FLAG_GLANCEABLE_HUB_V2_RESOURCES, + Flags.FLAG_HAPTICS_FOR_COMPOSE_SLIDERS, + Flags.FLAG_HARDWARE_COLOR_STYLES, + Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, + Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, + Flags.FLAG_HIDE_RINGER_BUTTON_IN_SINGLE_VOLUME_MODE, + Flags.FLAG_HOME_CONTROLS_DREAM_HSUM, + Flags.FLAG_HUB_EDIT_MODE_TOUCH_ADJUSTMENTS, + Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE, + Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, + Flags.FLAG_ICON_REFRESH_2025, + Flags.FLAG_IGNORE_TOUCHES_NEXT_TO_NOTIFICATION_SHELF, + Flags.FLAG_INDICATION_TEXT_A11Y_FIX, + Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, + Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, + Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_SHORTCUT_CUSTOMIZER, + Flags.FLAG_KEYBOARD_TOUCHPAD_CONTEXTUAL_EDUCATION, + Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF, + Flags.FLAG_KEYGUARD_WM_REORDER_ATMS_CALLS, + Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + Flags.FLAG_LOCKSCREEN_FONT, + Flags.FLAG_LOW_LIGHT_CLOCK_DREAM, + Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES, + Flags.FLAG_MEDIA_CONTROLS_A11Y_COLORS, + Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3, + Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3_PLACEMENT, + Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION, + Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE_BUGFIX, + Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, + Flags.FLAG_MEDIA_CONTROLS_UI_UPDATE, + Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND, + Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, + Flags.FLAG_MEDIA_LOAD_METADATA_VIA_MEDIA_DATA_LOADER, + Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION, + Flags.FLAG_MEDIA_PROJECTION_DIALOG_BEHIND_LOCKSCREEN, + Flags.FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT, + Flags.FLAG_MEDIA_PROJECTION_REQUEST_ATTRIBUTION_FIX, + Flags.FLAG_MODES_UI_DIALOG_PAGING, + Flags.FLAG_MOVE_TRANSITION_ANIMATION_LAYER, + Flags.FLAG_MSDL_FEEDBACK, + Flags.FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT, + Flags.FLAG_NEW_AOD_TRANSITION, + Flags.FLAG_NEW_VOLUME_PANEL, + Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING, + Flags.FLAG_NOTES_ROLE_QS_TILE, + Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS, + Flags.FLAG_NOTIFICATION_AMBIENT_SUPPRESSION_AFTER_INFLATION, + Flags.FLAG_NOTIFICATION_ANIMATED_ACTIONS_TREATMENT, + Flags.FLAG_NOTIFICATION_APPEAR_NONLINEAR, + Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, + Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, + Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, + Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, + Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_BUNDLE_UI, + Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, + Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, + Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, + Flags.FLAG_NOTIFICATION_REENTRANT_DISMISS, + Flags.FLAG_NOTIFICATION_ROW_ACCESSIBILITY_EXPANDED, + Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, + Flags.FLAG_NOTIFICATION_ROW_TRANSPARENCY, + Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, + Flags.FLAG_NOTIFICATION_SHADE_BLUR, + Flags.FLAG_NOTIFICATION_SHADE_UI_THREAD, + Flags.FLAG_NOTIFICATION_SKIP_SILENT_UPDATES, + Flags.FLAG_NOTIFICATION_TRANSPARENT_HEADER_FIX, + Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, + Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, + Flags.FLAG_NOTIFICATIONS_FOOTER_VISIBILITY_FIX, + Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, + Flags.FLAG_NOTIFICATIONS_HUN_SHARED_ANIMATION_VALUES, + Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, + Flags.FLAG_NOTIFICATIONS_LAUNCH_RADIUS, + Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, + Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE, + Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW, + Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS, + Flags.FLAG_NOTIFY_PASSWORD_TEXT_VIEW_USER_ACTIVITY_IN_BACKGROUND, + Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, + Flags.FLAG_ONLY_SHOW_MEDIA_STREAM_SLIDER_IN_SINGLE_VOLUME_MODE, + Flags.FLAG_OUTPUT_SWITCHER_REDESIGN, + Flags.FLAG_OVERRIDE_SUPPRESS_OVERLAY_CONDITION, + Flags.FLAG_PERMISSION_HELPER_INLINE_UI_RICH_ONGOING, + Flags.FLAG_PERMISSION_HELPER_UI_RICH_ONGOING, + Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT, + Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, + Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, + Flags.FLAG_PREDICTIVE_BACK_DELAY_WM_TRANSITION, + Flags.FLAG_PRIORITY_PEOPLE_SECTION, + Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, + Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, + Flags.FLAG_PSS_TASK_SWITCHER, + Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + Flags.FLAG_QS_NEW_TILES, + Flags.FLAG_QS_NEW_TILES_FUTURE, + Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES, + Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD, + Flags.FLAG_QS_TILE_DETAILED_VIEW, + Flags.FLAG_QS_TILE_FOCUS_STATE, + Flags.FLAG_QS_UI_REFACTOR, + Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT, + Flags.FLAG_RECORD_ISSUE_QS_TILE, + Flags.FLAG_REDESIGN_MAGNIFICATION_WINDOW_SIZE, + Flags.FLAG_REFACTOR_GET_CURRENT_USER, + Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, + Flags.FLAG_REGISTER_CONTENT_OBSERVERS_ASYNC, + Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, + Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, + Flags.FLAG_RELOCK_WITH_POWER_BUTTON_IMMEDIATELY, + Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, + Flags.FLAG_REMOVE_UPDATE_LISTENER_IN_QS_ICON_VIEW_IMPL, + Flags.FLAG_REST_TO_UNLOCK, + Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, + Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, + Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, + Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, + Flags.FLAG_SCENE_CONTAINER, + Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, + Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, + Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE, + Flags.FLAG_SCREENSHOT_POLICY_SPLIT_AND_DESKTOP_MODE, + Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, + Flags.FLAG_SCREENSHOT_UI_CONTROLLER_REFACTOR, + Flags.FLAG_SECONDARY_USER_WIDGET_HOST, + Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD, + Flags.FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS, + Flags.FLAG_SHADE_HEADER_FONT_UPDATE, + Flags.FLAG_SHADE_LAUNCH_ACCESSIBILITY, + Flags.FLAG_SHADE_WINDOW_GOES_AROUND, + Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, + Flags.FLAG_SHORTCUT_HELPER_KEY_GLYPH, + Flags.FLAG_SHOW_AUDIO_SHARING_SLIDER_IN_VOLUME_PANEL, + Flags.FLAG_SHOW_CLIPBOARD_INDICATION, + Flags.FLAG_SHOW_LOCKED_BY_YOUR_WATCH_KEYGUARD_INDICATOR, + Flags.FLAG_SHOW_TOAST_WHEN_APP_CONTROL_BRIGHTNESS, + Flags.FLAG_SIM_PIN_BOUNCER_RESET, + Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART, + Flags.FLAG_SIM_PIN_USE_SLOT_ID, + Flags.FLAG_SKIP_HIDE_SENSITIVE_NOTIF_ANIMATION, + Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, + Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, + Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, + Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, + Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING_FIX, + Flags.FLAG_SMARTSPACE_SWIPE_EVENT_LOGGING_FIX, + Flags.FLAG_SMARTSPACE_VIEWPAGER2, + Flags.FLAG_SOUNDDOSE_CUSTOMIZATION, + Flags.FLAG_SPATIAL_MODEL_APP_PUSHBACK, + Flags.FLAG_STABILIZE_HEADS_UP_GROUP_V2, + Flags.FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS, + Flags.FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP, + Flags.FLAG_STATUS_BAR_CHIPS_MODERNIZATION, + Flags.FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS, + Flags.FLAG_STATUS_BAR_FONT_UPDATES, + Flags.FLAG_STATUS_BAR_MOBILE_ICON_KAIROS, + Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, + Flags.FLAG_STATUS_BAR_NO_HUN_BEHAVIOR, + Flags.FLAG_STATUS_BAR_POPUP_CHIPS, + Flags.FLAG_STATUS_BAR_ROOT_MODERNIZATION, + Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP, + Flags.FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR, + Flags.FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR_ETHERNET, + Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, + Flags.FLAG_STATUS_BAR_STOP_UPDATING_WINDOW_HEIGHT, + Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP, + Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN, + Flags.FLAG_STATUS_BAR_UI_THREAD, + Flags.FLAG_STATUS_BAR_WINDOW_NO_CUSTOM_TOUCH, + Flags.FLAG_STOPPABLE_FGS_SYSTEM_APP, + Flags.FLAG_SWITCH_USER_ON_BG, + Flags.FLAG_SYSUI_TEAMFOOD, + Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, + Flags.FLAG_TRANSITION_RACE_CONDITION, + Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, + Flags.FLAG_TV_GLOBAL_ACTIONS_FOCUS, + Flags.FLAG_UDFPS_VIEW_PERFORMANCE, + Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, + Flags.FLAG_UNFOLD_LATENCY_TRACKING_FIX, + Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED, + Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, + Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY, + Flags.FLAG_USE_AAD_PROX_SENSOR, + Flags.FLAG_USE_NOTIF_INFLATION_THREAD_FOR_FOOTER, + Flags.FLAG_USE_NOTIF_INFLATION_THREAD_FOR_ROW, + Flags.FLAG_USE_TRANSITIONS_FOR_KEYGUARD_OCCLUDED, + Flags.FLAG_USE_VOLUME_CONTROLLER, + Flags.FLAG_USER_AWARE_SETTINGS_REPOSITORIES, + Flags.FLAG_USER_ENCRYPTED_SOURCE, + Flags.FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION, + Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR, + Flags.FLAG_VOLUME_REDESIGN, + "" + ) ); } diff --git a/flags/src/com/android/systemui/FakeFeatureFlagsImpl.java b/flags/src/com/android/systemui/FakeFeatureFlagsImpl.java index 0054d4f3a5..49523621c0 100644 --- a/flags/src/com/android/systemui/FakeFeatureFlagsImpl.java +++ b/flags/src/com/android/systemui/FakeFeatureFlagsImpl.java @@ -3,7 +3,6 @@ package com.android.systemui; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; - /** @hide */ public class FakeFeatureFlagsImpl extends CustomFeatureFlags { private final Map mFlagMap = new HashMap<>(); diff --git a/flags/src/com/android/systemui/FeatureFlags.java b/flags/src/com/android/systemui/FeatureFlags.java index 6ce51d50c3..681e59c0eb 100644 --- a/flags/src/com/android/systemui/FeatureFlags.java +++ b/flags/src/com/android/systemui/FeatureFlags.java @@ -3,308 +3,1124 @@ package com.android.systemui; /** @hide */ public interface FeatureFlags { - + + + boolean activityTransitionUseLargestWindow(); - + + + + boolean addBlackBackgroundForWindowMagnifier(); + + + + boolean alwaysComposeQsUiFragment(); + + + boolean ambientTouchMonitorListenToDisplayChanges(); - + + + boolean appClipsBacklinks(); - + + + + boolean appShortcutRemovalFix(); + + + + boolean avalancheReplaceHunWhenCritical(); + + + boolean bindKeyguardMediaVisibility(); - - boolean bpTalkback(); - + + + + boolean bouncerUiRevamp(); + + + + boolean bouncerUiRevamp2(); + + + + boolean bpColors(); + + + boolean brightnessSliderFocusState(); - - boolean centralizedStatusBarHeightFix(); - + + + + boolean checkLockscreenGoneTransition(); + + + + boolean classicFlagsMultiUser(); + + + + boolean clipboardImageTimeout(); + + + boolean clipboardNoninteractiveOnLockscreen(); - - boolean clockReactiveVariants(); - + + + + boolean clipboardOverlayMultiuser(); + + + + boolean clipboardSharedTransitions(); + + + + boolean clipboardUseDescriptionMimetype(); + + + + boolean clockFidgetAnimation(); + + + boolean communalBouncerDoNotModifyPluginOpen(); - + + + + boolean communalEditWidgetsActivityFinishFix(); + + + boolean communalHub(); - + + + + boolean communalHubUseThreadPoolForWidgets(); + + + + boolean communalResponsiveGrid(); + + + + boolean communalSceneKtfRefactor(); + + + + boolean communalStandaloneSupport(); + + + + boolean communalTimerFlickerFix(); + + + + boolean communalWidgetResizing(); + + + + boolean communalWidgetTrampolineFix(); + + + boolean composeBouncer(); - - boolean composeLockscreen(); - + + + boolean confineNotificationTouchToViewWidth(); - - boolean constraintBp(); - + + + + boolean contAuthPlugin(); + + + boolean contextualTipsAssistantDismissFix(); - + + + boolean coroutineTracing(); - + + + boolean createWindowlessWindowMagnifier(); - - boolean dedicatedNotifInflationThread(); - + + + + boolean debugLiveUpdatesPromoteAll(); + + + + boolean decoupleViewControllerInAnimlib(); + + + boolean delayShowMagnificationButton(); - - boolean delayedWakelockReleaseOnBackgroundThread(); - + + + + boolean desktopEffectsQsTile(); + + + boolean deviceEntryUdfpsRefactor(); - + + + + boolean disableBlurredShadeVisible(); + + + boolean disableContextualTipsFrequencyCheck(); - + + + boolean disableContextualTipsIosSwitcherCheck(); - - boolean dozeuiSchedulingAlarmsBackgroundExecution(); - + + + + boolean disableShadeTrackpadTwoFingerSwipe(); + + + + boolean doubleTapToSleep(); + + + boolean dreamInputSessionPilferOnce(); - + + + boolean dreamOverlayBouncerSwipeDirectionFiltering(); - - boolean dualShade(); - + + + + boolean dreamOverlayUpdatedFont(); + + + boolean edgeBackGestureHandlerThread(); - + + + boolean edgebackGestureHandlerGetRunningTasksBackground(); - + + + boolean enableBackgroundKeyguardOndrawnCallback(); - + + + boolean enableContextualTipForMuteVolume(); - + + + boolean enableContextualTipForPowerOff(); - + + + boolean enableContextualTipForTakeScreenshot(); - + + + boolean enableContextualTips(); - + + + boolean enableEfficientDisplayRepository(); - + + + boolean enableLayoutTracing(); - + + + + boolean enableUnderlay(); + + + boolean enableViewCaptureTracing(); - - boolean enableWidgetPickerSizeFilter(); - + + + boolean enforceBrightnessBaseUserRestriction(); - + + + boolean exampleFlag(); - - boolean fastUnlockTransition(); - + + + + boolean expandCollapsePrivacyDialog(); + + + + boolean expandHeadsUpOnInlineReply(); + + + + boolean expandedPrivacyIndicatorsOnLargeScreen(); + + + + boolean extendedAppsShortcutCategory(); + + + + boolean faceMessageDeferUpdate(); + + + + boolean faceScanningAnimationNpeFix(); + + + + boolean fasterUnlockTransition(); + + + + boolean fetchBookmarksXmlKeyboardShortcuts(); + + + boolean fixImageWallpaperCrashSurfaceAlreadyReleased(); - + + + boolean fixScreenshotActionDismissSystemWindows(); - + + + boolean floatingMenuAnimatedTuck(); - + + + + boolean floatingMenuDisplayCutoutSupport(); + + + boolean floatingMenuDragToEdit(); - + + + boolean floatingMenuDragToHide(); - + + + + boolean floatingMenuHearingDeviceStatusIcon(); + + + boolean floatingMenuImeDisplacementAnimation(); - + + + boolean floatingMenuNarrowTargetContentObserver(); - + + + + boolean floatingMenuNotifyTargetsChangedOnStrictDiff(); + + + boolean floatingMenuOverlapsNavBarsFlag(); - + + + boolean floatingMenuRadiiAnimation(); - - boolean generatedPreviews(); - + + + boolean getConnectedDeviceNameUnsynchronized(); - + + + boolean glanceableHubAllowKeyguardWhenDreaming(); - - boolean glanceableHubFullscreenSwipe(); - - boolean glanceableHubGestureHandle(); - - boolean glanceableHubShortcutButton(); - - boolean hapticBrightnessSlider(); - - boolean hapticVolumeSlider(); - + + + + boolean glanceableHubBlurredBackground(); + + + + boolean glanceableHubDirectEditMode(); + + + + boolean glanceableHubV2(); + + + + boolean glanceableHubV2Resources(); + + + + boolean hapticsForComposeSliders(); + + + + boolean hardwareColorStyles(); + + + boolean hearingAidsQsTileDialog(); - + + + boolean hearingDevicesDialogRelatedTools(); - + + + + boolean hideRingerButtonInSingleVolumeMode(); + + + + boolean homeControlsDreamHsum(); + + + + boolean hubEditModeTouchAdjustments(); + + + + boolean hubmodeFullscreenVerticalSwipe(); + + + + boolean hubmodeFullscreenVerticalSwipeFix(); + + + + boolean iconRefresh2025(); + + + + boolean ignoreTouchesNextToNotificationShelf(); + + + + boolean indicationTextA11yFix(); + + + boolean keyboardDockingIndicator(); - + + + boolean keyboardShortcutHelperRewrite(); - - boolean keyguardBottomAreaRefactor(); - + + + + boolean keyboardShortcutHelperShortcutCustomizer(); + + + + boolean keyboardTouchpadContextualEducation(); + + + + boolean keyguardTransitionForceFinishOnScreenOff(); + + + + boolean keyguardWmReorderAtmsCalls(); + + + boolean keyguardWmStateRefactor(); - - boolean lightRevealMigration(); - + + + + boolean lockscreenFont(); + + + + boolean lowLightClockDream(); + + + + boolean magneticNotificationSwipes(); + + + + boolean mediaControlsA11yColors(); + + + + boolean mediaControlsButtonMedia3(); + + + + boolean mediaControlsButtonMedia3Placement(); + + + + boolean mediaControlsDeviceManagerBackgroundExecution(); + + + + boolean mediaControlsDrawablesReuseBugfix(); + + + boolean mediaControlsLockscreenShadeBugFix(); - - boolean mediaControlsRefactor(); - + + + + boolean mediaControlsUiUpdate(); + + + + boolean mediaControlsUmoInflationInBackground(); + + + boolean mediaControlsUserInitiatedDeleteintent(); - - boolean migrateClocksToBlueprint(); - + + + + boolean mediaLoadMetadataViaMediaDataLoader(); + + + + boolean mediaLockscreenLaunchAnimation(); + + + + boolean mediaProjectionDialogBehindLockscreen(); + + + + boolean mediaProjectionGreyErrorText(); + + + + boolean mediaProjectionRequestAttributionFix(); + + + + boolean modesUiDialogPaging(); + + + + boolean moveTransitionAnimationLayer(); + + + + boolean msdlFeedback(); + + + + boolean multiuserWifiPickerTrackerSupport(); + + + boolean newAodTransition(); - - boolean newTouchpadGesturesTutorial(); - + + + boolean newVolumePanel(); - + + + + boolean nonTouchscreenDevicesBypassFalsing(); + + + + boolean notesRoleQsTile(); + + + + boolean notificationAddXOnHoverToDismiss(); + + + + boolean notificationAmbientSuppressionAfterInflation(); + + + + boolean notificationAnimatedActionsTreatment(); + + + + boolean notificationAppearNonlinear(); + + + boolean notificationAsyncGroupHeaderInflation(); - + + + boolean notificationAsyncHybridViewInflation(); - + + + boolean notificationAvalancheSuppression(); - + + + boolean notificationAvalancheThrottleHun(); - + + + boolean notificationBackgroundTintOptimization(); - + + + + boolean notificationBundleUi(); + + + boolean notificationColorUpdateLogger(); - + + + boolean notificationContentAlphaOptimization(); - + + + boolean notificationFooterBackgroundTintOptimization(); - - boolean notificationMediaManagerBackgroundExecution(); - - boolean notificationMinimalismPrototype(); - + + + boolean notificationOverExpansionClippingFix(); - - boolean notificationPulsingFix(); - + + + + boolean notificationReentrantDismiss(); + + + + boolean notificationRowAccessibilityExpanded(); + + + boolean notificationRowContentBinderRefactor(); - + + + + boolean notificationRowTransparency(); + + + boolean notificationRowUserContext(); - + + + + boolean notificationShadeBlur(); + + + + boolean notificationShadeUiThread(); + + + + boolean notificationSkipSilentUpdates(); + + + + boolean notificationTransparentHeaderFix(); + + + boolean notificationViewFlipperPausingV2(); - + + + boolean notificationsBackgroundIcons(); - - boolean notificationsFooterViewRefactor(); - - boolean notificationsHeadsUpRefactor(); - + + + + boolean notificationsFooterVisibilityFix(); + + + boolean notificationsHideOnDisplaySwitch(); - + + + + boolean notificationsHunSharedAnimationValues(); + + + boolean notificationsIconContainerRefactor(); - - boolean notificationsImprovedHunAnimation(); - + + + + boolean notificationsLaunchRadius(); + + + boolean notificationsLiveDataStoreRefactor(); - + + + + boolean notificationsPinnedHunInShade(); + + + + boolean notificationsRedesignFooterView(); + + + + boolean notificationsRedesignGuts(); + + + + boolean notifyPasswordTextViewUserActivityInBackground(); + + + boolean notifyPowerManagerUserActivityBackground(); - + + + + boolean onlyShowMediaStreamSliderInSingleVolumeMode(); + + + + boolean outputSwitcherRedesign(); + + + + boolean overrideSuppressOverlayCondition(); + + + + boolean permissionHelperInlineUiRichOngoing(); + + + + boolean permissionHelperUiRichOngoing(); + + + + boolean physicalNotificationMovement(); + + + boolean pinInputFieldStyledFocusState(); - - boolean predictiveBackAnimateBouncer(); - - boolean predictiveBackAnimateDialogs(); - + + + boolean predictiveBackAnimateShade(); - - boolean predictiveBackSysui(); - + + + + boolean predictiveBackDelayWmTransition(); + + + boolean priorityPeopleSection(); - - boolean privacyDotUnfoldWrongCornerFix(); - - boolean pssAppSelectorAbruptExitFix(); - + + + + boolean promoteNotificationsAutomatically(); + + + boolean pssAppSelectorRecentsSplitScreen(); - + + + boolean pssTaskSwitcher(); - + + + boolean qsCustomTileClickGuaranteedBugFix(); - - boolean qsNewPipeline(); - + + + boolean qsNewTiles(); - + + + boolean qsNewTilesFuture(); - + + + + boolean qsQuickRebindActiveTiles(); + + + + boolean qsRegisterSettingObserverOnBgThread(); + + + + boolean qsTileDetailedView(); + + + boolean qsTileFocusState(); - + + + boolean qsUiRefactor(); - - boolean quickSettingsVisualHapticsLongpress(); - + + + + boolean qsUiRefactorComposeFragment(); + + + boolean recordIssueQsTile(); - + + + + boolean redesignMagnificationWindowSize(); + + + boolean refactorGetCurrentUser(); - + + + boolean registerBatteryControllerReceiversInCorestartable(); - + + + + boolean registerContentObserversAsync(); + + + boolean registerNewWalletCardInBackground(); - + + + boolean registerWallpaperNotifierBackground(); - - boolean registerZenModeContentObserverBackground(); - + + + + boolean relockWithPowerButtonImmediately(); + + + boolean removeDreamOverlayHideOnTouch(); - + + + + boolean removeUpdateListenerInQsIconViewImpl(); + + + boolean restToUnlock(); - + + + boolean restartDreamOnUnocclude(); - + + + boolean revampedBouncerMessages(); - + + + boolean runFingerprintDetectOnDismissibleKeyguard(); - + + + boolean saveAndRestoreMagnificationSettingsButtons(); - + + + boolean sceneContainer(); - + + + boolean screenshareNotificationHidingBugFix(); - + + + boolean screenshotActionDismissSystemWindows(); - - boolean screenshotPrivateProfileAccessibilityAnnouncementFix(); - - boolean screenshotPrivateProfileBehaviorFix(); - + + + + boolean screenshotMultidisplayFocusChange(); + + + + boolean screenshotPolicySplitAndDesktopMode(); + + + boolean screenshotScrollCropViewCrashFix(); - - boolean screenshotShelfUi2(); - - boolean shadeCollapseActivityLaunchFix(); - + + + + boolean screenshotUiControllerRefactor(); + + + + boolean secondaryUserWidgetHost(); + + + + boolean settingsExtRegisterContentObserverOnBgThread(); + + + + boolean shadeExpandsOnStatusBarLongPress(); + + + + boolean shadeHeaderFontUpdate(); + + + + boolean shadeLaunchAccessibility(); + + + + boolean shadeWindowGoesAround(); + + + boolean shaderlibLoadingEffectRefactor(); - + + + + boolean shortcutHelperKeyGlyph(); + + + + boolean showAudioSharingSliderInVolumePanel(); + + + + boolean showClipboardIndication(); + + + + boolean showLockedByYourWatchKeyguardIndicator(); + + + + boolean showToastWhenAppControlBrightness(); + + + + boolean simPinBouncerReset(); + + + + boolean simPinRaceConditionOnRestart(); + + + + boolean simPinUseSlotId(); + + + + boolean skipHideSensitiveNotifAnimation(); + + + boolean sliceBroadcastRelayInBackground(); - + + + boolean sliceManagerBinderCallBackground(); - + + + boolean smartspaceLockscreenViewmodel(); - + + + boolean smartspaceRelocateToBottom(); - - boolean smartspaceRemoteviewsRendering(); - + + + + boolean smartspaceRemoteviewsRenderingFix(); + + + + boolean smartspaceSwipeEventLoggingFix(); + + + + boolean smartspaceViewpager2(); + + + + boolean sounddoseCustomization(); + + + + boolean spatialModelAppPushback(); + + + + boolean stabilizeHeadsUpGroupV2(); + + + + boolean statusBarAlwaysCheckUnderlyingNetworks(); + + + + boolean statusBarAutoStartScreenRecordChip(); + + + + boolean statusBarChipsModernization(); + + + + boolean statusBarChipsReturnAnimations(); + + + + boolean statusBarFontUpdates(); + + + + boolean statusBarMobileIconKairos(); + + + boolean statusBarMonochromeIconsFix(); - - boolean statusBarScreenSharingChips(); - + + + + boolean statusBarNoHunBehavior(); + + + + boolean statusBarPopupChips(); + + + + boolean statusBarRootModernization(); + + + + boolean statusBarShowAudioOnlyProjectionChip(); + + + + boolean statusBarSignalPolicyRefactor(); + + + + boolean statusBarSignalPolicyRefactorEthernet(); + + + boolean statusBarStaticInoutIndicators(); - + + + + boolean statusBarStopUpdatingWindowHeight(); + + + + boolean statusBarSwipeOverChip(); + + + + boolean statusBarSwitchToSpnFromDataSpn(); + + + + boolean statusBarUiThread(); + + + + boolean statusBarWindowNoCustomTouch(); + + + + boolean stoppableFgsSystemApp(); + + + boolean switchUserOnBg(); - + + + boolean sysuiTeamfood(); - + + + boolean themeOverlayControllerWakefulnessDeprecation(); - + + + + boolean transitionRaceCondition(); + + + boolean translucentOccludingActivityFix(); - - boolean truncatedStatusBarIconsFix(); - + + + + boolean tvGlobalActionsFocus(); + + + boolean udfpsViewPerformance(); - + + + boolean unfoldAnimationBackgroundProgress(); - + + + + boolean unfoldLatencyTrackingFix(); + + + + boolean updateCornerRadiusOnDisplayChanged(); + + + boolean updateUserSwitcherBackground(); - - boolean validateKeyboardShortcutHelperIconUri(); - + + + + boolean updateWindowMagnifierBottomBoundary(); + + + + boolean useAadProxSensor(); + + + + boolean useNotifInflationThreadForFooter(); + + + + boolean useNotifInflationThreadForRow(); + + + + boolean useTransitionsForKeyguardOccluded(); + + + + boolean useVolumeController(); + + + + boolean userAwareSettingsRepositories(); + + + + boolean userEncryptedSource(); + + + + boolean userSwitcherAddSignOutOption(); + + + boolean visualInterruptionsRefactor(); + + + + boolean volumeRedesign(); } diff --git a/flags/src/com/android/systemui/FeatureFlagsImpl.java b/flags/src/com/android/systemui/FeatureFlagsImpl.java index 32049600d5..7dbc6b650e 100644 --- a/flags/src/com/android/systemui/FeatureFlagsImpl.java +++ b/flags/src/com/android/systemui/FeatureFlagsImpl.java @@ -1,3178 +1,1965 @@ package com.android.systemui; // TODO(b/303773055): Remove the annotation after access issue is resolved. - -import com.android.quickstep.util.DeviceConfigHelper; - -import java.nio.file.Files; -import java.nio.file.Paths; /** @hide */ public final class FeatureFlagsImpl implements FeatureFlags { - private static final boolean isReadFromNew = Files.exists(Paths.get("/metadata/aconfig/boot/enable_only_new_storage")); - private static volatile boolean isCached = false; - private static volatile boolean accessibility_is_cached = false; - private static volatile boolean biometrics_framework_is_cached = false; - private static volatile boolean communal_is_cached = false; - private static volatile boolean systemui_is_cached = false; - private static boolean activityTransitionUseLargestWindow = true; - private static boolean ambientTouchMonitorListenToDisplayChanges = false; - private static boolean appClipsBacklinks = false; - private static boolean bindKeyguardMediaVisibility = true; - private static boolean bpTalkback = true; - private static boolean brightnessSliderFocusState = false; - private static boolean centralizedStatusBarHeightFix = true; - private static boolean clipboardNoninteractiveOnLockscreen = false; - private static boolean clockReactiveVariants = false; - private static boolean communalBouncerDoNotModifyPluginOpen = false; - private static boolean communalHub = true; - private static boolean composeBouncer = false; - private static boolean composeLockscreen = false; - private static boolean confineNotificationTouchToViewWidth = true; - private static boolean constraintBp = true; - private static boolean contextualTipsAssistantDismissFix = true; - private static boolean coroutineTracing = true; - private static boolean createWindowlessWindowMagnifier = true; - private static boolean dedicatedNotifInflationThread = true; - private static boolean delayShowMagnificationButton = true; - private static boolean delayedWakelockReleaseOnBackgroundThread = true; - private static boolean deviceEntryUdfpsRefactor = true; - private static boolean disableContextualTipsFrequencyCheck = true; - private static boolean disableContextualTipsIosSwitcherCheck = true; - private static boolean dozeuiSchedulingAlarmsBackgroundExecution = false; - private static boolean dreamInputSessionPilferOnce = false; - private static boolean dreamOverlayBouncerSwipeDirectionFiltering = true; - private static boolean dualShade = false; - private static boolean edgeBackGestureHandlerThread = false; - private static boolean edgebackGestureHandlerGetRunningTasksBackground = true; - private static boolean enableBackgroundKeyguardOndrawnCallback = true; - private static boolean enableContextualTipForMuteVolume = false; - private static boolean enableContextualTipForPowerOff = true; - private static boolean enableContextualTipForTakeScreenshot = true; - private static boolean enableContextualTips = true; - private static boolean enableEfficientDisplayRepository = false; - private static boolean enableLayoutTracing = false; - private static boolean enableViewCaptureTracing = false; - private static boolean enableWidgetPickerSizeFilter = false; - private static boolean enforceBrightnessBaseUserRestriction = true; - private static boolean exampleFlag = false; - private static boolean fastUnlockTransition = false; - private static boolean fixImageWallpaperCrashSurfaceAlreadyReleased = true; - private static boolean fixScreenshotActionDismissSystemWindows = true; - private static boolean floatingMenuAnimatedTuck = true; - private static boolean floatingMenuDragToEdit = true; - private static boolean floatingMenuDragToHide = false; - private static boolean floatingMenuImeDisplacementAnimation = true; - private static boolean floatingMenuNarrowTargetContentObserver = true; - private static boolean floatingMenuOverlapsNavBarsFlag = true; - private static boolean floatingMenuRadiiAnimation = true; - private static boolean generatedPreviews = true; - private static boolean getConnectedDeviceNameUnsynchronized = true; - private static boolean glanceableHubAllowKeyguardWhenDreaming = false; - private static boolean glanceableHubFullscreenSwipe = false; - private static boolean glanceableHubGestureHandle = false; - private static boolean glanceableHubShortcutButton = false; - private static boolean hapticBrightnessSlider = true; - private static boolean hapticVolumeSlider = true; - private static boolean hearingAidsQsTileDialog = true; - private static boolean hearingDevicesDialogRelatedTools = true; - private static boolean keyboardDockingIndicator = true; - private static boolean keyboardShortcutHelperRewrite = false; - private static boolean keyguardBottomAreaRefactor = true; - private static boolean keyguardWmStateRefactor = false; - private static boolean lightRevealMigration = true; - private static boolean mediaControlsLockscreenShadeBugFix = true; - private static boolean mediaControlsRefactor = true; - private static boolean mediaControlsUserInitiatedDeleteintent = true; - private static boolean migrateClocksToBlueprint = true; - private static boolean newAodTransition = true; - private static boolean newTouchpadGesturesTutorial = false; - private static boolean newVolumePanel = true; - private static boolean notificationAsyncGroupHeaderInflation = true; - private static boolean notificationAsyncHybridViewInflation = true; - private static boolean notificationAvalancheSuppression = true; - private static boolean notificationAvalancheThrottleHun = true; - private static boolean notificationBackgroundTintOptimization = true; - private static boolean notificationColorUpdateLogger = false; - private static boolean notificationContentAlphaOptimization = true; - private static boolean notificationFooterBackgroundTintOptimization = false; - private static boolean notificationMediaManagerBackgroundExecution = true; - private static boolean notificationMinimalismPrototype = false; - private static boolean notificationOverExpansionClippingFix = true; - private static boolean notificationPulsingFix = true; - private static boolean notificationRowContentBinderRefactor = false; - private static boolean notificationRowUserContext = true; - private static boolean notificationViewFlipperPausingV2 = true; - private static boolean notificationsBackgroundIcons = false; - private static boolean notificationsFooterViewRefactor = true; - private static boolean notificationsHeadsUpRefactor = true; - private static boolean notificationsHideOnDisplaySwitch = false; - private static boolean notificationsIconContainerRefactor = true; - private static boolean notificationsImprovedHunAnimation = true; - private static boolean notificationsLiveDataStoreRefactor = true; - private static boolean notifyPowerManagerUserActivityBackground = true; - private static boolean pinInputFieldStyledFocusState = true; - private static boolean predictiveBackAnimateBouncer = true; - private static boolean predictiveBackAnimateDialogs = true; - private static boolean predictiveBackAnimateShade = false; - private static boolean predictiveBackSysui = true; - private static boolean priorityPeopleSection = true; - private static boolean privacyDotUnfoldWrongCornerFix = true; - private static boolean pssAppSelectorAbruptExitFix = true; - private static boolean pssAppSelectorRecentsSplitScreen = true; - private static boolean pssTaskSwitcher = false; - private static boolean qsCustomTileClickGuaranteedBugFix = true; - private static boolean qsNewPipeline = true; - private static boolean qsNewTiles = false; - private static boolean qsNewTilesFuture = false; - private static boolean qsTileFocusState = true; - private static boolean qsUiRefactor = false; - private static boolean quickSettingsVisualHapticsLongpress = true; - private static boolean recordIssueQsTile = true; - private static boolean refactorGetCurrentUser = true; - private static boolean registerBatteryControllerReceiversInCorestartable = false; - private static boolean registerNewWalletCardInBackground = true; - private static boolean registerWallpaperNotifierBackground = true; - private static boolean registerZenModeContentObserverBackground = true; - private static boolean removeDreamOverlayHideOnTouch = true; - private static boolean restToUnlock = false; - private static boolean restartDreamOnUnocclude = false; - private static boolean revampedBouncerMessages = true; - private static boolean runFingerprintDetectOnDismissibleKeyguard = true; - private static boolean saveAndRestoreMagnificationSettingsButtons = false; - private static boolean sceneContainer = false; - private static boolean screenshareNotificationHidingBugFix = true; - private static boolean screenshotActionDismissSystemWindows = true; - private static boolean screenshotPrivateProfileAccessibilityAnnouncementFix = true; - private static boolean screenshotPrivateProfileBehaviorFix = true; - private static boolean screenshotScrollCropViewCrashFix = true; - private static boolean screenshotShelfUi2 = true; - private static boolean shadeCollapseActivityLaunchFix = false; - private static boolean shaderlibLoadingEffectRefactor = true; - private static boolean sliceBroadcastRelayInBackground = true; - private static boolean sliceManagerBinderCallBackground = true; - private static boolean smartspaceLockscreenViewmodel = true; - private static boolean smartspaceRelocateToBottom = false; - private static boolean smartspaceRemoteviewsRendering = false; - private static boolean statusBarMonochromeIconsFix = true; - private static boolean statusBarScreenSharingChips = true; - private static boolean statusBarStaticInoutIndicators = false; - private static boolean switchUserOnBg = true; - private static boolean sysuiTeamfood = true; - private static boolean themeOverlayControllerWakefulnessDeprecation = false; - private static boolean translucentOccludingActivityFix = false; - private static boolean truncatedStatusBarIconsFix = true; - private static boolean udfpsViewPerformance = true; - private static boolean unfoldAnimationBackgroundProgress = true; - private static boolean updateUserSwitcherBackground = true; - private static boolean validateKeyboardShortcutHelperIconUri = true; - private static boolean visualInterruptionsRefactor = true; - - - private void init() { - boolean foundPackage = true; - - createWindowlessWindowMagnifier = foundPackage; - - - delayShowMagnificationButton = foundPackage; - - - floatingMenuAnimatedTuck = foundPackage; - - - floatingMenuDragToEdit = foundPackage; - - - floatingMenuDragToHide = foundPackage; - - - floatingMenuImeDisplacementAnimation = foundPackage; - - - floatingMenuNarrowTargetContentObserver = foundPackage; - - - floatingMenuOverlapsNavBarsFlag = foundPackage; - - - floatingMenuRadiiAnimation = foundPackage; - - - hearingDevicesDialogRelatedTools = foundPackage; - - saveAndRestoreMagnificationSettingsButtons = foundPackage; - bpTalkback = foundPackage; - constraintBp = foundPackage; - communalHub = foundPackage; - enableWidgetPickerSizeFilter = foundPackage; - activityTransitionUseLargestWindow = foundPackage; - ambientTouchMonitorListenToDisplayChanges = foundPackage; - appClipsBacklinks = foundPackage; - bindKeyguardMediaVisibility = foundPackage; - brightnessSliderFocusState = foundPackage; - centralizedStatusBarHeightFix = foundPackage; - clipboardNoninteractiveOnLockscreen = foundPackage; - clockReactiveVariants = foundPackage; - communalBouncerDoNotModifyPluginOpen = foundPackage; - composeBouncer = foundPackage; - composeLockscreen = foundPackage; - confineNotificationTouchToViewWidth = foundPackage; - contextualTipsAssistantDismissFix = foundPackage; - coroutineTracing = foundPackage; - dedicatedNotifInflationThread = foundPackage; - delayedWakelockReleaseOnBackgroundThread = foundPackage; - deviceEntryUdfpsRefactor = foundPackage; - disableContextualTipsFrequencyCheck = foundPackage; - disableContextualTipsIosSwitcherCheck = foundPackage; - dozeuiSchedulingAlarmsBackgroundExecution = foundPackage; - dreamInputSessionPilferOnce = foundPackage; - dreamOverlayBouncerSwipeDirectionFiltering = foundPackage; - dualShade = foundPackage; - edgeBackGestureHandlerThread = foundPackage; - edgebackGestureHandlerGetRunningTasksBackground = foundPackage; - enableBackgroundKeyguardOndrawnCallback = foundPackage; - enableContextualTipForMuteVolume = foundPackage; - enableContextualTipForPowerOff = foundPackage; - enableContextualTipForTakeScreenshot = foundPackage; - enableContextualTips = foundPackage; - enableEfficientDisplayRepository = foundPackage; - enableLayoutTracing = foundPackage; - enableViewCaptureTracing = foundPackage; - enforceBrightnessBaseUserRestriction = foundPackage; - exampleFlag = foundPackage; - fastUnlockTransition = foundPackage; - fixImageWallpaperCrashSurfaceAlreadyReleased = foundPackage; - fixScreenshotActionDismissSystemWindows = foundPackage; - generatedPreviews = foundPackage; - getConnectedDeviceNameUnsynchronized = foundPackage; - glanceableHubAllowKeyguardWhenDreaming = foundPackage; - glanceableHubFullscreenSwipe = foundPackage; - glanceableHubGestureHandle = foundPackage; - glanceableHubShortcutButton = foundPackage; - hapticBrightnessSlider = foundPackage; - hapticVolumeSlider = foundPackage; - hearingAidsQsTileDialog = foundPackage; - keyboardDockingIndicator = foundPackage; - keyboardShortcutHelperRewrite = foundPackage; - keyguardBottomAreaRefactor = foundPackage; - keyguardWmStateRefactor = foundPackage; - lightRevealMigration = foundPackage; - mediaControlsLockscreenShadeBugFix = foundPackage; - mediaControlsRefactor = foundPackage; - mediaControlsUserInitiatedDeleteintent = foundPackage; - migrateClocksToBlueprint = foundPackage; - newAodTransition = foundPackage; - newTouchpadGesturesTutorial = foundPackage; - newVolumePanel = foundPackage; - notificationAsyncGroupHeaderInflation = foundPackage; - notificationAsyncHybridViewInflation = foundPackage; - notificationAvalancheSuppression = foundPackage; - notificationAvalancheThrottleHun = foundPackage; - notificationBackgroundTintOptimization = foundPackage; - notificationColorUpdateLogger = foundPackage; - notificationContentAlphaOptimization = foundPackage; - notificationFooterBackgroundTintOptimization = foundPackage; - notificationMediaManagerBackgroundExecution = foundPackage; - notificationMinimalismPrototype = foundPackage; - notificationOverExpansionClippingFix = foundPackage; - notificationPulsingFix = foundPackage; - notificationRowContentBinderRefactor = foundPackage; - notificationRowUserContext = foundPackage; - notificationViewFlipperPausingV2 = foundPackage; - notificationsBackgroundIcons = foundPackage; - notificationsFooterViewRefactor = foundPackage; - notificationsHeadsUpRefactor = foundPackage; - notificationsHideOnDisplaySwitch = foundPackage; - notificationsIconContainerRefactor = foundPackage; - notificationsImprovedHunAnimation = foundPackage; - notificationsLiveDataStoreRefactor = foundPackage; - notifyPowerManagerUserActivityBackground = foundPackage; - pinInputFieldStyledFocusState = foundPackage; - predictiveBackAnimateBouncer = foundPackage; - predictiveBackAnimateDialogs = foundPackage; - predictiveBackAnimateShade = foundPackage; - predictiveBackSysui = foundPackage; - priorityPeopleSection = foundPackage; - privacyDotUnfoldWrongCornerFix = foundPackage; - pssAppSelectorAbruptExitFix = foundPackage; - pssAppSelectorRecentsSplitScreen = foundPackage; - pssTaskSwitcher = foundPackage; - qsCustomTileClickGuaranteedBugFix = foundPackage; - qsNewPipeline = foundPackage; - qsNewTiles = foundPackage; - qsNewTilesFuture = foundPackage; - qsTileFocusState = foundPackage; - qsUiRefactor = foundPackage; - quickSettingsVisualHapticsLongpress = foundPackage; - recordIssueQsTile = foundPackage; - refactorGetCurrentUser = foundPackage; - registerBatteryControllerReceiversInCorestartable = foundPackage; - registerNewWalletCardInBackground = foundPackage; - registerWallpaperNotifierBackground = foundPackage; - registerZenModeContentObserverBackground = foundPackage; - removeDreamOverlayHideOnTouch = foundPackage; - restToUnlock = foundPackage; - restartDreamOnUnocclude = foundPackage; - revampedBouncerMessages = foundPackage; - runFingerprintDetectOnDismissibleKeyguard = foundPackage; - sceneContainer = foundPackage; - screenshareNotificationHidingBugFix = foundPackage; - screenshotActionDismissSystemWindows = foundPackage; - screenshotPrivateProfileAccessibilityAnnouncementFix = foundPackage; - screenshotPrivateProfileBehaviorFix = foundPackage; - screenshotScrollCropViewCrashFix = foundPackage; - screenshotShelfUi2 = foundPackage; - shadeCollapseActivityLaunchFix = foundPackage; - shaderlibLoadingEffectRefactor = foundPackage; - sliceBroadcastRelayInBackground = foundPackage; - sliceManagerBinderCallBackground = foundPackage; - smartspaceLockscreenViewmodel = foundPackage; - smartspaceRelocateToBottom = foundPackage; - smartspaceRemoteviewsRendering = foundPackage; - - - statusBarMonochromeIconsFix = foundPackage; - - - statusBarScreenSharingChips = foundPackage; - - - statusBarStaticInoutIndicators = foundPackage; - - - switchUserOnBg = foundPackage; - - - sysuiTeamfood = foundPackage; - - - themeOverlayControllerWakefulnessDeprecation = foundPackage; - - - translucentOccludingActivityFix = foundPackage; - - - truncatedStatusBarIconsFix = foundPackage; - - - udfpsViewPerformance = foundPackage; - - - unfoldAnimationBackgroundProgress = foundPackage; - - - updateUserSwitcherBackground = foundPackage; - - - validateKeyboardShortcutHelperIconUri = foundPackage; - - - visualInterruptionsRefactor = foundPackage; - - isCached = true; - } - - - - - private void load_overrides_accessibility() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - createWindowlessWindowMagnifier = - properties.getBoolean(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER, true); - delayShowMagnificationButton = - properties.getBoolean(Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON, true); - floatingMenuAnimatedTuck = - properties.getBoolean(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK, true); - floatingMenuDragToEdit = - properties.getBoolean(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT, true); - floatingMenuDragToHide = - properties.getBoolean(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE, false); - floatingMenuImeDisplacementAnimation = - properties.getBoolean(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION, true); - floatingMenuNarrowTargetContentObserver = - properties.getBoolean(Flags.FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER, true); - floatingMenuOverlapsNavBarsFlag = - properties.getBoolean(Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG, true); - floatingMenuRadiiAnimation = - properties.getBoolean(Flags.FLAG_FLOATING_MENU_RADII_ANIMATION, true); - hearingDevicesDialogRelatedTools = - properties.getBoolean(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS, true); - saveAndRestoreMagnificationSettingsButtons = - properties.getBoolean(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS, false); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace accessibility " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - accessibility_is_cached = true; - } - - private void load_overrides_biometrics_framework() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - bpTalkback = - properties.getBoolean(Flags.FLAG_BP_TALKBACK, true); - constraintBp = - properties.getBoolean(Flags.FLAG_CONSTRAINT_BP, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace biometrics_framework " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - biometrics_framework_is_cached = true; - } - - private void load_overrides_communal() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - communalHub = - properties.getBoolean(Flags.FLAG_COMMUNAL_HUB, true); - enableWidgetPickerSizeFilter = - properties.getBoolean(Flags.FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER, false); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace communal " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - communal_is_cached = true; - } - - private void load_overrides_systemui() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - activityTransitionUseLargestWindow = - properties.getBoolean(Flags.FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW, true); - ambientTouchMonitorListenToDisplayChanges = - properties.getBoolean(Flags.FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES, false); - appClipsBacklinks = - properties.getBoolean(Flags.FLAG_APP_CLIPS_BACKLINKS, false); - bindKeyguardMediaVisibility = - properties.getBoolean(Flags.FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY, true); - brightnessSliderFocusState = - properties.getBoolean(Flags.FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE, false); - centralizedStatusBarHeightFix = - properties.getBoolean(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX, true); - clipboardNoninteractiveOnLockscreen = - properties.getBoolean(Flags.FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN, false); - clockReactiveVariants = - properties.getBoolean(Flags.FLAG_CLOCK_REACTIVE_VARIANTS, false); - communalBouncerDoNotModifyPluginOpen = - properties.getBoolean(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN, false); - composeBouncer = - properties.getBoolean(Flags.FLAG_COMPOSE_BOUNCER, false); - composeLockscreen = - properties.getBoolean(Flags.FLAG_COMPOSE_LOCKSCREEN, false); - confineNotificationTouchToViewWidth = - properties.getBoolean(Flags.FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH, true); - contextualTipsAssistantDismissFix = - properties.getBoolean(Flags.FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX, true); - coroutineTracing = - properties.getBoolean(Flags.FLAG_COROUTINE_TRACING, true); - dedicatedNotifInflationThread = - properties.getBoolean(Flags.FLAG_DEDICATED_NOTIF_INFLATION_THREAD, true); - delayedWakelockReleaseOnBackgroundThread = - properties.getBoolean(Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD, true); - deviceEntryUdfpsRefactor = - properties.getBoolean(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, true); - disableContextualTipsFrequencyCheck = - properties.getBoolean(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK, true); - disableContextualTipsIosSwitcherCheck = - properties.getBoolean(Flags.FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK, true); - dozeuiSchedulingAlarmsBackgroundExecution = - properties.getBoolean(Flags.FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION, false); - dreamInputSessionPilferOnce = - properties.getBoolean(Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE, false); - dreamOverlayBouncerSwipeDirectionFiltering = - properties.getBoolean(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING, true); - dualShade = - properties.getBoolean(Flags.FLAG_DUAL_SHADE, false); - edgeBackGestureHandlerThread = - properties.getBoolean(Flags.FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD, false); - edgebackGestureHandlerGetRunningTasksBackground = - properties.getBoolean(Flags.FLAG_EDGEBACK_GESTURE_HANDLER_GET_RUNNING_TASKS_BACKGROUND, true); - enableBackgroundKeyguardOndrawnCallback = - properties.getBoolean(Flags.FLAG_ENABLE_BACKGROUND_KEYGUARD_ONDRAWN_CALLBACK, true); - enableContextualTipForMuteVolume = - properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_MUTE_VOLUME, false); - enableContextualTipForPowerOff = - properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_POWER_OFF, true); - enableContextualTipForTakeScreenshot = - properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIP_FOR_TAKE_SCREENSHOT, true); - enableContextualTips = - properties.getBoolean(Flags.FLAG_ENABLE_CONTEXTUAL_TIPS, true); - enableEfficientDisplayRepository = - properties.getBoolean(Flags.FLAG_ENABLE_EFFICIENT_DISPLAY_REPOSITORY, false); - enableLayoutTracing = - properties.getBoolean(Flags.FLAG_ENABLE_LAYOUT_TRACING, false); - enableViewCaptureTracing = - properties.getBoolean(Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING, false); - enforceBrightnessBaseUserRestriction = - properties.getBoolean(Flags.FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION, true); - exampleFlag = - properties.getBoolean(Flags.FLAG_EXAMPLE_FLAG, false); - fastUnlockTransition = - properties.getBoolean(Flags.FLAG_FAST_UNLOCK_TRANSITION, false); - fixImageWallpaperCrashSurfaceAlreadyReleased = - properties.getBoolean(Flags.FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED, true); - fixScreenshotActionDismissSystemWindows = - properties.getBoolean(Flags.FLAG_FIX_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, true); - generatedPreviews = - properties.getBoolean(Flags.FLAG_GENERATED_PREVIEWS, true); - getConnectedDeviceNameUnsynchronized = - properties.getBoolean(Flags.FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED, true); - glanceableHubAllowKeyguardWhenDreaming = - properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING, false); - glanceableHubFullscreenSwipe = - properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE, false); - glanceableHubGestureHandle = - properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_GESTURE_HANDLE, false); - glanceableHubShortcutButton = - properties.getBoolean(Flags.FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON, false); - hapticBrightnessSlider = - properties.getBoolean(Flags.FLAG_HAPTIC_BRIGHTNESS_SLIDER, true); - hapticVolumeSlider = - properties.getBoolean(Flags.FLAG_HAPTIC_VOLUME_SLIDER, true); - hearingAidsQsTileDialog = - properties.getBoolean(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG, true); - keyboardDockingIndicator = - properties.getBoolean(Flags.FLAG_KEYBOARD_DOCKING_INDICATOR, true); - keyboardShortcutHelperRewrite = - properties.getBoolean(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE, false); - keyguardBottomAreaRefactor = - properties.getBoolean(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, true); - keyguardWmStateRefactor = - properties.getBoolean(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, false); - lightRevealMigration = - properties.getBoolean(Flags.FLAG_LIGHT_REVEAL_MIGRATION, true); - mediaControlsLockscreenShadeBugFix = - properties.getBoolean(Flags.FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX, true); - mediaControlsRefactor = - properties.getBoolean(Flags.FLAG_MEDIA_CONTROLS_REFACTOR, true); - mediaControlsUserInitiatedDeleteintent = - properties.getBoolean(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT, true); - migrateClocksToBlueprint = - properties.getBoolean(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, true); - newAodTransition = - properties.getBoolean(Flags.FLAG_NEW_AOD_TRANSITION, true); - newTouchpadGesturesTutorial = - properties.getBoolean(Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, false); - newVolumePanel = - properties.getBoolean(Flags.FLAG_NEW_VOLUME_PANEL, true); - notificationAsyncGroupHeaderInflation = - properties.getBoolean(Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION, true); - notificationAsyncHybridViewInflation = - properties.getBoolean(Flags.FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION, true); - notificationAvalancheSuppression = - properties.getBoolean(Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION, true); - notificationAvalancheThrottleHun = - properties.getBoolean(Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN, true); - notificationBackgroundTintOptimization = - properties.getBoolean(Flags.FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION, true); - notificationColorUpdateLogger = - properties.getBoolean(Flags.FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER, false); - notificationContentAlphaOptimization = - properties.getBoolean(Flags.FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION, true); - notificationFooterBackgroundTintOptimization = - properties.getBoolean(Flags.FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION, false); - notificationMediaManagerBackgroundExecution = - properties.getBoolean(Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION, true); - notificationMinimalismPrototype = - properties.getBoolean(Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE, false); - notificationOverExpansionClippingFix = - properties.getBoolean(Flags.FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX, true); - notificationPulsingFix = - properties.getBoolean(Flags.FLAG_NOTIFICATION_PULSING_FIX, true); - notificationRowContentBinderRefactor = - properties.getBoolean(Flags.FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR, false); - notificationRowUserContext = - properties.getBoolean(Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT, true); - notificationViewFlipperPausingV2 = - properties.getBoolean(Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2, true); - notificationsBackgroundIcons = - properties.getBoolean(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS, false); - notificationsFooterViewRefactor = - properties.getBoolean(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR, true); - notificationsHeadsUpRefactor = - properties.getBoolean(Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR, true); - notificationsHideOnDisplaySwitch = - properties.getBoolean(Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH, false); - notificationsIconContainerRefactor = - properties.getBoolean(Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR, true); - notificationsImprovedHunAnimation = - properties.getBoolean(Flags.FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION, true); - notificationsLiveDataStoreRefactor = - properties.getBoolean(Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR, true); - notifyPowerManagerUserActivityBackground = - properties.getBoolean(Flags.FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND, true); - pinInputFieldStyledFocusState = - properties.getBoolean(Flags.FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE, true); - predictiveBackAnimateBouncer = - properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER, true); - predictiveBackAnimateDialogs = - properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS, true); - predictiveBackAnimateShade = - properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_SHADE, false); - predictiveBackSysui = - properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_SYSUI, true); - priorityPeopleSection = - properties.getBoolean(Flags.FLAG_PRIORITY_PEOPLE_SECTION, true); - privacyDotUnfoldWrongCornerFix = - properties.getBoolean(Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX, true); - pssAppSelectorAbruptExitFix = - properties.getBoolean(Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX, true); - pssAppSelectorRecentsSplitScreen = - properties.getBoolean(Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN, true); - pssTaskSwitcher = - properties.getBoolean(Flags.FLAG_PSS_TASK_SWITCHER, false); - qsCustomTileClickGuaranteedBugFix = - properties.getBoolean(Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, true); - qsNewPipeline = - properties.getBoolean(Flags.FLAG_QS_NEW_PIPELINE, true); - qsNewTiles = - properties.getBoolean(Flags.FLAG_QS_NEW_TILES, false); - qsNewTilesFuture = - properties.getBoolean(Flags.FLAG_QS_NEW_TILES_FUTURE, false); - qsTileFocusState = - properties.getBoolean(Flags.FLAG_QS_TILE_FOCUS_STATE, true); - qsUiRefactor = - properties.getBoolean(Flags.FLAG_QS_UI_REFACTOR, false); - quickSettingsVisualHapticsLongpress = - properties.getBoolean(Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS, true); - recordIssueQsTile = - properties.getBoolean(Flags.FLAG_RECORD_ISSUE_QS_TILE, true); - refactorGetCurrentUser = - properties.getBoolean(Flags.FLAG_REFACTOR_GET_CURRENT_USER, true); - registerBatteryControllerReceiversInCorestartable = - properties.getBoolean(Flags.FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE, false); - registerNewWalletCardInBackground = - properties.getBoolean(Flags.FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND, true); - registerWallpaperNotifierBackground = - properties.getBoolean(Flags.FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND, true); - registerZenModeContentObserverBackground = - properties.getBoolean(Flags.FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND, true); - removeDreamOverlayHideOnTouch = - properties.getBoolean(Flags.FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH, true); - restToUnlock = - properties.getBoolean(Flags.FLAG_REST_TO_UNLOCK, false); - restartDreamOnUnocclude = - properties.getBoolean(Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE, false); - revampedBouncerMessages = - properties.getBoolean(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES, true); - runFingerprintDetectOnDismissibleKeyguard = - properties.getBoolean(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD, true); - sceneContainer = - properties.getBoolean(Flags.FLAG_SCENE_CONTAINER, false); - screenshareNotificationHidingBugFix = - properties.getBoolean(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, true); - screenshotActionDismissSystemWindows = - properties.getBoolean(Flags.FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS, true); - screenshotPrivateProfileAccessibilityAnnouncementFix = - properties.getBoolean(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX, true); - screenshotPrivateProfileBehaviorFix = - properties.getBoolean(Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX, true); - screenshotScrollCropViewCrashFix = - properties.getBoolean(Flags.FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX, true); - screenshotShelfUi2 = - properties.getBoolean(Flags.FLAG_SCREENSHOT_SHELF_UI2, true); - shadeCollapseActivityLaunchFix = - properties.getBoolean(Flags.FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX, false); - shaderlibLoadingEffectRefactor = - properties.getBoolean(Flags.FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR, true); - sliceBroadcastRelayInBackground = - properties.getBoolean(Flags.FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND, true); - sliceManagerBinderCallBackground = - properties.getBoolean(Flags.FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND, true); - smartspaceLockscreenViewmodel = - properties.getBoolean(Flags.FLAG_SMARTSPACE_LOCKSCREEN_VIEWMODEL, true); - smartspaceRelocateToBottom = - properties.getBoolean(Flags.FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM, false); - smartspaceRemoteviewsRendering = - properties.getBoolean(Flags.FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING, false); - statusBarMonochromeIconsFix = - properties.getBoolean(Flags.FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX, true); - statusBarScreenSharingChips = - properties.getBoolean(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, true); - statusBarStaticInoutIndicators = - properties.getBoolean(Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS, false); - switchUserOnBg = - properties.getBoolean(Flags.FLAG_SWITCH_USER_ON_BG, true); - sysuiTeamfood = - properties.getBoolean(Flags.FLAG_SYSUI_TEAMFOOD, true); - themeOverlayControllerWakefulnessDeprecation = - properties.getBoolean(Flags.FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION, false); - translucentOccludingActivityFix = - properties.getBoolean(Flags.FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX, false); - truncatedStatusBarIconsFix = - properties.getBoolean(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX, true); - udfpsViewPerformance = - properties.getBoolean(Flags.FLAG_UDFPS_VIEW_PERFORMANCE, true); - unfoldAnimationBackgroundProgress = - properties.getBoolean(Flags.FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS, true); - updateUserSwitcherBackground = - properties.getBoolean(Flags.FLAG_UPDATE_USER_SWITCHER_BACKGROUND, true); - validateKeyboardShortcutHelperIconUri = - properties.getBoolean(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI, true); - visualInterruptionsRefactor = - properties.getBoolean(Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace systemui " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - systemui_is_cached = true; - } - @Override - + + public boolean activityTransitionUseLargestWindow() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return activityTransitionUseLargestWindow; - + return true; } @Override - + + + public boolean addBlackBackgroundForWindowMagnifier() { + return true; + } + + @Override + + + public boolean alwaysComposeQsUiFragment() { + return false; + } + + @Override + + public boolean ambientTouchMonitorListenToDisplayChanges() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return ambientTouchMonitorListenToDisplayChanges; - + return true; } @Override - + + public boolean appClipsBacklinks() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return appClipsBacklinks; - + return true; } @Override - + + + public boolean appShortcutRemovalFix() { + return true; + } + + @Override + + + public boolean avalancheReplaceHunWhenCritical() { + return false; + } + + @Override + + public boolean bindKeyguardMediaVisibility() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return bindKeyguardMediaVisibility; - + return true; } @Override - - public boolean bpTalkback() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!biometrics_framework_is_cached) { - load_overrides_biometrics_framework(); - } - } - return bpTalkback; + + public boolean bouncerUiRevamp() { + return false; } @Override - + + + public boolean bouncerUiRevamp2() { + return false; + } + + @Override + + + public boolean bpColors() { + return false; + } + + @Override + + public boolean brightnessSliderFocusState() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return brightnessSliderFocusState; - + return false; } @Override - - public boolean centralizedStatusBarHeightFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return centralizedStatusBarHeightFix; + + public boolean checkLockscreenGoneTransition() { + return true; } @Override - + + + public boolean classicFlagsMultiUser() { + return true; + } + + @Override + + + public boolean clipboardImageTimeout() { + return true; + } + + @Override + + public boolean clipboardNoninteractiveOnLockscreen() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return clipboardNoninteractiveOnLockscreen; - + return true; } @Override - - public boolean clockReactiveVariants() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return clockReactiveVariants; + + public boolean clipboardOverlayMultiuser() { + return false; } @Override - + + + public boolean clipboardSharedTransitions() { + return true; + } + + @Override + + + public boolean clipboardUseDescriptionMimetype() { + return true; + } + + @Override + + + public boolean clockFidgetAnimation() { + return false; + } + + @Override + + public boolean communalBouncerDoNotModifyPluginOpen() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return communalBouncerDoNotModifyPluginOpen; - + return true; } @Override - + + + public boolean communalEditWidgetsActivityFinishFix() { + return true; + } + + @Override + + public boolean communalHub() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!communal_is_cached) { - load_overrides_communal(); - } - } - return communalHub; - + return true; } @Override - + + + public boolean communalHubUseThreadPoolForWidgets() { + return true; + } + + @Override + + + public boolean communalResponsiveGrid() { + return false; + } + + @Override + + + public boolean communalSceneKtfRefactor() { + return true; + } + + @Override + + + public boolean communalStandaloneSupport() { + return false; + } + + @Override + + + public boolean communalTimerFlickerFix() { + return true; + } + + @Override + + + public boolean communalWidgetResizing() { + return true; + } + + @Override + + + public boolean communalWidgetTrampolineFix() { + return true; + } + + @Override + + public boolean composeBouncer() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return composeBouncer; - + return false; } @Override - - public boolean composeLockscreen() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return composeLockscreen; - } - @Override - public boolean confineNotificationTouchToViewWidth() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return confineNotificationTouchToViewWidth; - + return false; } @Override - - public boolean constraintBp() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!biometrics_framework_is_cached) { - load_overrides_biometrics_framework(); - } - } - return constraintBp; + + public boolean contAuthPlugin() { + return false; } @Override - + + public boolean contextualTipsAssistantDismissFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return contextualTipsAssistantDismissFix; - + return true; } @Override - + + public boolean coroutineTracing() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return coroutineTracing; - + return false; } @Override - + + public boolean createWindowlessWindowMagnifier() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return createWindowlessWindowMagnifier; - + return true; } @Override - - public boolean dedicatedNotifInflationThread() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return dedicatedNotifInflationThread; + + public boolean debugLiveUpdatesPromoteAll() { + return false; } @Override - + + + public boolean decoupleViewControllerInAnimlib() { + return false; + } + + @Override + + public boolean delayShowMagnificationButton() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return delayShowMagnificationButton; - + return true; } @Override - - public boolean delayedWakelockReleaseOnBackgroundThread() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return delayedWakelockReleaseOnBackgroundThread; + + public boolean desktopEffectsQsTile() { + return false; } @Override - + + public boolean deviceEntryUdfpsRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return deviceEntryUdfpsRefactor; - + return true; } @Override - + + + public boolean disableBlurredShadeVisible() { + return false; + } + + @Override + + public boolean disableContextualTipsFrequencyCheck() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return disableContextualTipsFrequencyCheck; - + return false; } @Override - + + public boolean disableContextualTipsIosSwitcherCheck() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return disableContextualTipsIosSwitcherCheck; - + return false; } @Override - - public boolean dozeuiSchedulingAlarmsBackgroundExecution() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return dozeuiSchedulingAlarmsBackgroundExecution; + + public boolean disableShadeTrackpadTwoFingerSwipe() { + return false; } @Override - + + + public boolean doubleTapToSleep() { + return false; + } + + @Override + + public boolean dreamInputSessionPilferOnce() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return dreamInputSessionPilferOnce; - + return true; } @Override - + + public boolean dreamOverlayBouncerSwipeDirectionFiltering() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return dreamOverlayBouncerSwipeDirectionFiltering; - + return true; } @Override - - public boolean dualShade() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return dualShade; + + public boolean dreamOverlayUpdatedFont() { + return false; } @Override - + + public boolean edgeBackGestureHandlerThread() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return edgeBackGestureHandlerThread; - + return false; } @Override - + + public boolean edgebackGestureHandlerGetRunningTasksBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return edgebackGestureHandlerGetRunningTasksBackground; - + return true; } @Override - + + public boolean enableBackgroundKeyguardOndrawnCallback() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableBackgroundKeyguardOndrawnCallback; - + return true; } @Override - + + public boolean enableContextualTipForMuteVolume() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableContextualTipForMuteVolume; - + return true; } @Override - + + public boolean enableContextualTipForPowerOff() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableContextualTipForPowerOff; - + return true; } @Override - + + public boolean enableContextualTipForTakeScreenshot() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableContextualTipForTakeScreenshot; - + return true; } @Override - + + public boolean enableContextualTips() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableContextualTips; - + return true; } @Override - + + public boolean enableEfficientDisplayRepository() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableEfficientDisplayRepository; - + return true; } @Override - + + public boolean enableLayoutTracing() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableLayoutTracing; - + return false; } @Override - + + + public boolean enableUnderlay() { + return false; + } + + @Override + + public boolean enableViewCaptureTracing() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableViewCaptureTracing; - + return false; } @Override - - public boolean enableWidgetPickerSizeFilter() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!communal_is_cached) { - load_overrides_communal(); - } - } - return enableWidgetPickerSizeFilter; - } - @Override - public boolean enforceBrightnessBaseUserRestriction() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enforceBrightnessBaseUserRestriction; - + return true; } @Override - + + public boolean exampleFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return exampleFlag; - + return false; } @Override - - public boolean fastUnlockTransition() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return fastUnlockTransition; + + public boolean expandCollapsePrivacyDialog() { + return true; } @Override - + + + public boolean expandHeadsUpOnInlineReply() { + return true; + } + + @Override + + + public boolean expandedPrivacyIndicatorsOnLargeScreen() { + return false; + } + + @Override + + + public boolean extendedAppsShortcutCategory() { + return false; + } + + @Override + + + public boolean faceMessageDeferUpdate() { + return true; + } + + @Override + + + public boolean faceScanningAnimationNpeFix() { + return true; + } + + @Override + + + public boolean fasterUnlockTransition() { + return true; + } + + @Override + + + public boolean fetchBookmarksXmlKeyboardShortcuts() { + return true; + } + + @Override + + public boolean fixImageWallpaperCrashSurfaceAlreadyReleased() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return fixImageWallpaperCrashSurfaceAlreadyReleased; - + return true; } @Override - + + public boolean fixScreenshotActionDismissSystemWindows() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return fixScreenshotActionDismissSystemWindows; - + return true; } @Override - + + public boolean floatingMenuAnimatedTuck() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return floatingMenuAnimatedTuck; - + return false; } @Override - + + + public boolean floatingMenuDisplayCutoutSupport() { + return true; + } + + @Override + + public boolean floatingMenuDragToEdit() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return floatingMenuDragToEdit; - + return true; } @Override - + + public boolean floatingMenuDragToHide() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return floatingMenuDragToHide; - + return false; } @Override - + + + public boolean floatingMenuHearingDeviceStatusIcon() { + return false; + } + + @Override + + public boolean floatingMenuImeDisplacementAnimation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return floatingMenuImeDisplacementAnimation; - + return false; } @Override - + + public boolean floatingMenuNarrowTargetContentObserver() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return floatingMenuNarrowTargetContentObserver; - + return true; } @Override - + + + public boolean floatingMenuNotifyTargetsChangedOnStrictDiff() { + return true; + } + + @Override + + public boolean floatingMenuOverlapsNavBarsFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return floatingMenuOverlapsNavBarsFlag; - + return false; } @Override - + + public boolean floatingMenuRadiiAnimation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return floatingMenuRadiiAnimation; - - } - - @Override - - public boolean generatedPreviews() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return generatedPreviews; - + return false; } @Override - + + public boolean getConnectedDeviceNameUnsynchronized() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return getConnectedDeviceNameUnsynchronized; - + return true; } @Override - + + public boolean glanceableHubAllowKeyguardWhenDreaming() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return glanceableHubAllowKeyguardWhenDreaming; - + return false; } @Override - - public boolean glanceableHubFullscreenSwipe() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return glanceableHubFullscreenSwipe; + + public boolean glanceableHubBlurredBackground() { + return false; } @Override - - public boolean glanceableHubGestureHandle() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return glanceableHubGestureHandle; + + public boolean glanceableHubDirectEditMode() { + return false; } @Override - - public boolean glanceableHubShortcutButton() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return glanceableHubShortcutButton; + + public boolean glanceableHubV2() { + return false; } @Override - - public boolean hapticBrightnessSlider() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return hapticBrightnessSlider; + + public boolean glanceableHubV2Resources() { + return false; } @Override - - public boolean hapticVolumeSlider() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return hapticVolumeSlider; + + public boolean hapticsForComposeSliders() { + return true; } @Override - + + + public boolean hardwareColorStyles() { + return false; + } + + @Override + + public boolean hearingAidsQsTileDialog() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return hearingAidsQsTileDialog; - + return true; } @Override - + + public boolean hearingDevicesDialogRelatedTools() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return hearingDevicesDialogRelatedTools; - + return true; } @Override - + + + public boolean hideRingerButtonInSingleVolumeMode() { + return false; + } + + @Override + + + public boolean homeControlsDreamHsum() { + return true; + } + + @Override + + + public boolean hubEditModeTouchAdjustments() { + return false; + } + + @Override + + + public boolean hubmodeFullscreenVerticalSwipe() { + return false; + } + + @Override + + + public boolean hubmodeFullscreenVerticalSwipeFix() { + return true; + } + + @Override + + + public boolean iconRefresh2025() { + return false; + } + + @Override + + + public boolean ignoreTouchesNextToNotificationShelf() { + return true; + } + + @Override + + + public boolean indicationTextA11yFix() { + return true; + } + + @Override + + public boolean keyboardDockingIndicator() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return keyboardDockingIndicator; - + return false; } @Override - + + public boolean keyboardShortcutHelperRewrite() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return keyboardShortcutHelperRewrite; - + return true; } @Override - - public boolean keyguardBottomAreaRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return keyguardBottomAreaRefactor; + + public boolean keyboardShortcutHelperShortcutCustomizer() { + return true; } @Override - + + + public boolean keyboardTouchpadContextualEducation() { + return true; + } + + @Override + + + public boolean keyguardTransitionForceFinishOnScreenOff() { + return false; + } + + @Override + + + public boolean keyguardWmReorderAtmsCalls() { + return true; + } + + @Override + + public boolean keyguardWmStateRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return keyguardWmStateRefactor; - + return false; } @Override - - public boolean lightRevealMigration() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return lightRevealMigration; + + public boolean lockscreenFont() { + return false; } @Override - + + + public boolean lowLightClockDream() { + return false; + } + + @Override + + + public boolean magneticNotificationSwipes() { + return false; + } + + @Override + + + public boolean mediaControlsA11yColors() { + return true; + } + + @Override + + + public boolean mediaControlsButtonMedia3() { + return false; + } + + @Override + + + public boolean mediaControlsButtonMedia3Placement() { + return false; + } + + @Override + + + public boolean mediaControlsDeviceManagerBackgroundExecution() { + return false; + } + + @Override + + + public boolean mediaControlsDrawablesReuseBugfix() { + return true; + } + + @Override + + public boolean mediaControlsLockscreenShadeBugFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return mediaControlsLockscreenShadeBugFix; - + return true; } @Override - - public boolean mediaControlsRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return mediaControlsRefactor; + + public boolean mediaControlsUiUpdate() { + return false; } @Override - + + + public boolean mediaControlsUmoInflationInBackground() { + return true; + } + + @Override + + public boolean mediaControlsUserInitiatedDeleteintent() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return mediaControlsUserInitiatedDeleteintent; - + return true; } @Override - - public boolean migrateClocksToBlueprint() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return migrateClocksToBlueprint; + + public boolean mediaLoadMetadataViaMediaDataLoader() { + return true; } @Override - + + + public boolean mediaLockscreenLaunchAnimation() { + return true; + } + + @Override + + + public boolean mediaProjectionDialogBehindLockscreen() { + return true; + } + + @Override + + + public boolean mediaProjectionGreyErrorText() { + return true; + } + + @Override + + + public boolean mediaProjectionRequestAttributionFix() { + return false; + } + + @Override + + + public boolean modesUiDialogPaging() { + return false; + } + + @Override + + + public boolean moveTransitionAnimationLayer() { + return false; + } + + @Override + + + public boolean msdlFeedback() { + return false; + } + + @Override + + + public boolean multiuserWifiPickerTrackerSupport() { + return false; + } + + @Override + + public boolean newAodTransition() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return newAodTransition; - + return true; } @Override - - public boolean newTouchpadGesturesTutorial() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return newTouchpadGesturesTutorial; - } - @Override - public boolean newVolumePanel() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return newVolumePanel; - + return true; } @Override - + + + public boolean nonTouchscreenDevicesBypassFalsing() { + return false; + } + + @Override + + + public boolean notesRoleQsTile() { + return false; + } + + @Override + + + public boolean notificationAddXOnHoverToDismiss() { + return false; + } + + @Override + + + public boolean notificationAmbientSuppressionAfterInflation() { + return false; + } + + @Override + + + public boolean notificationAnimatedActionsTreatment() { + return false; + } + + @Override + + + public boolean notificationAppearNonlinear() { + return true; + } + + @Override + + public boolean notificationAsyncGroupHeaderInflation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationAsyncGroupHeaderInflation; - + return true; } @Override - + + public boolean notificationAsyncHybridViewInflation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationAsyncHybridViewInflation; - + return true; } @Override - + + public boolean notificationAvalancheSuppression() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationAvalancheSuppression; - + return true; } @Override - + + public boolean notificationAvalancheThrottleHun() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationAvalancheThrottleHun; - + return true; } @Override - + + public boolean notificationBackgroundTintOptimization() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationBackgroundTintOptimization; - + return true; } @Override - + + + public boolean notificationBundleUi() { + return false; + } + + @Override + + public boolean notificationColorUpdateLogger() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationColorUpdateLogger; - + return false; } @Override - + + public boolean notificationContentAlphaOptimization() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationContentAlphaOptimization; - + return false; } @Override - + + public boolean notificationFooterBackgroundTintOptimization() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationFooterBackgroundTintOptimization; - + return false; } @Override - - public boolean notificationMediaManagerBackgroundExecution() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationMediaManagerBackgroundExecution; - } - @Override - - public boolean notificationMinimalismPrototype() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationMinimalismPrototype; - - } - - @Override - public boolean notificationOverExpansionClippingFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationOverExpansionClippingFix; - + return true; } @Override - - public boolean notificationPulsingFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationPulsingFix; + + public boolean notificationReentrantDismiss() { + return true; } @Override - + + + public boolean notificationRowAccessibilityExpanded() { + return true; + } + + @Override + + public boolean notificationRowContentBinderRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationRowContentBinderRefactor; - + return true; } @Override - + + + public boolean notificationRowTransparency() { + return false; + } + + @Override + + public boolean notificationRowUserContext() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationRowUserContext; - + return true; } @Override - + + + public boolean notificationShadeBlur() { + return false; + } + + @Override + + + public boolean notificationShadeUiThread() { + return false; + } + + @Override + + + public boolean notificationSkipSilentUpdates() { + return false; + } + + @Override + + + public boolean notificationTransparentHeaderFix() { + return true; + } + + @Override + + public boolean notificationViewFlipperPausingV2() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationViewFlipperPausingV2; - + return true; } @Override - + + public boolean notificationsBackgroundIcons() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationsBackgroundIcons; - + return true; } @Override - - public boolean notificationsFooterViewRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationsFooterViewRefactor; + + public boolean notificationsFooterVisibilityFix() { + return true; } @Override - - public boolean notificationsHeadsUpRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationsHeadsUpRefactor; - } - @Override - public boolean notificationsHideOnDisplaySwitch() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationsHideOnDisplaySwitch; - + return false; } @Override - + + + public boolean notificationsHunSharedAnimationValues() { + return false; + } + + @Override + + public boolean notificationsIconContainerRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationsIconContainerRefactor; - + return true; } @Override - - public boolean notificationsImprovedHunAnimation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationsImprovedHunAnimation; + + public boolean notificationsLaunchRadius() { + return false; } @Override - + + public boolean notificationsLiveDataStoreRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notificationsLiveDataStoreRefactor; - + return true; } @Override - + + + public boolean notificationsPinnedHunInShade() { + return true; + } + + @Override + + + public boolean notificationsRedesignFooterView() { + return false; + } + + @Override + + + public boolean notificationsRedesignGuts() { + return false; + } + + @Override + + + public boolean notifyPasswordTextViewUserActivityInBackground() { + return true; + } + + @Override + + public boolean notifyPowerManagerUserActivityBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return notifyPowerManagerUserActivityBackground; - + return true; } @Override - + + + public boolean onlyShowMediaStreamSliderInSingleVolumeMode() { + return true; + } + + @Override + + + public boolean outputSwitcherRedesign() { + return false; + } + + @Override + + + public boolean overrideSuppressOverlayCondition() { + return false; + } + + @Override + + + public boolean permissionHelperInlineUiRichOngoing() { + return false; + } + + @Override + + + public boolean permissionHelperUiRichOngoing() { + return false; + } + + @Override + + + public boolean physicalNotificationMovement() { + return false; + } + + @Override + + public boolean pinInputFieldStyledFocusState() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return pinInputFieldStyledFocusState; - + return true; } @Override - - public boolean predictiveBackAnimateBouncer() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return predictiveBackAnimateBouncer; - } - @Override - - public boolean predictiveBackAnimateDialogs() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return predictiveBackAnimateDialogs; - - } - - @Override - public boolean predictiveBackAnimateShade() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return predictiveBackAnimateShade; - + return false; } @Override - - public boolean predictiveBackSysui() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return predictiveBackSysui; + + public boolean predictiveBackDelayWmTransition() { + return false; } @Override - + + public boolean priorityPeopleSection() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return priorityPeopleSection; - + return true; } @Override - - public boolean privacyDotUnfoldWrongCornerFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return privacyDotUnfoldWrongCornerFix; + + public boolean promoteNotificationsAutomatically() { + return false; } @Override - - public boolean pssAppSelectorAbruptExitFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return pssAppSelectorAbruptExitFix; - } - @Override - public boolean pssAppSelectorRecentsSplitScreen() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return pssAppSelectorRecentsSplitScreen; - + return true; } @Override - + + public boolean pssTaskSwitcher() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return pssTaskSwitcher; - + return false; } @Override - + + public boolean qsCustomTileClickGuaranteedBugFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return qsCustomTileClickGuaranteedBugFix; - + return true; } @Override - - public boolean qsNewPipeline() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return qsNewPipeline; - } - @Override - public boolean qsNewTiles() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return qsNewTiles; - + return false; } @Override - + + public boolean qsNewTilesFuture() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return qsNewTilesFuture; - + return false; } @Override - + + + public boolean qsQuickRebindActiveTiles() { + return false; + } + + @Override + + + public boolean qsRegisterSettingObserverOnBgThread() { + return true; + } + + @Override + + + public boolean qsTileDetailedView() { + return false; + } + + @Override + + public boolean qsTileFocusState() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return qsTileFocusState; - + return true; } @Override - + + public boolean qsUiRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return qsUiRefactor; - + return false; } @Override - - public boolean quickSettingsVisualHapticsLongpress() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return quickSettingsVisualHapticsLongpress; + + public boolean qsUiRefactorComposeFragment() { + return false; } @Override - + + public boolean recordIssueQsTile() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return recordIssueQsTile; - + return true; } @Override - + + + public boolean redesignMagnificationWindowSize() { + return false; + } + + @Override + + public boolean refactorGetCurrentUser() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return refactorGetCurrentUser; - + return true; } @Override - + + public boolean registerBatteryControllerReceiversInCorestartable() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return registerBatteryControllerReceiversInCorestartable; - + return false; } @Override - + + + public boolean registerContentObserversAsync() { + return true; + } + + @Override + + public boolean registerNewWalletCardInBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return registerNewWalletCardInBackground; - + return true; } @Override - + + public boolean registerWallpaperNotifierBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return registerWallpaperNotifierBackground; - + return true; } @Override - - public boolean registerZenModeContentObserverBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return registerZenModeContentObserverBackground; + + public boolean relockWithPowerButtonImmediately() { + return true; } @Override - + + public boolean removeDreamOverlayHideOnTouch() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return removeDreamOverlayHideOnTouch; - + return true; } @Override - + + + public boolean removeUpdateListenerInQsIconViewImpl() { + return true; + } + + @Override + + public boolean restToUnlock() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return restToUnlock; - + return false; } @Override - + + public boolean restartDreamOnUnocclude() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return restartDreamOnUnocclude; - + return false; } @Override - + + public boolean revampedBouncerMessages() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return revampedBouncerMessages; - + return true; } @Override - + + public boolean runFingerprintDetectOnDismissibleKeyguard() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return runFingerprintDetectOnDismissibleKeyguard; - + return false; } @Override - + + public boolean saveAndRestoreMagnificationSettingsButtons() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return saveAndRestoreMagnificationSettingsButtons; - + return true; } @Override - + + public boolean sceneContainer() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return sceneContainer; - + return false; } @Override - + + public boolean screenshareNotificationHidingBugFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return screenshareNotificationHidingBugFix; - + return true; } @Override - + + public boolean screenshotActionDismissSystemWindows() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return screenshotActionDismissSystemWindows; - + return false; } @Override - - public boolean screenshotPrivateProfileAccessibilityAnnouncementFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return screenshotPrivateProfileAccessibilityAnnouncementFix; + + public boolean screenshotMultidisplayFocusChange() { + return false; } @Override - - public boolean screenshotPrivateProfileBehaviorFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return screenshotPrivateProfileBehaviorFix; + + public boolean screenshotPolicySplitAndDesktopMode() { + return true; } @Override - + + public boolean screenshotScrollCropViewCrashFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return screenshotScrollCropViewCrashFix; - + return true; } @Override - - public boolean screenshotShelfUi2() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return screenshotShelfUi2; + + public boolean screenshotUiControllerRefactor() { + return true; } @Override - - public boolean shadeCollapseActivityLaunchFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return shadeCollapseActivityLaunchFix; + + public boolean secondaryUserWidgetHost() { + return false; } @Override - + + + public boolean settingsExtRegisterContentObserverOnBgThread() { + return true; + } + + @Override + + + public boolean shadeExpandsOnStatusBarLongPress() { + return true; + } + + @Override + + + public boolean shadeHeaderFontUpdate() { + return false; + } + + @Override + + + public boolean shadeLaunchAccessibility() { + return true; + } + + @Override + + + public boolean shadeWindowGoesAround() { + return false; + } + + @Override + + public boolean shaderlibLoadingEffectRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return shaderlibLoadingEffectRefactor; - + return true; } @Override - + + + public boolean shortcutHelperKeyGlyph() { + return true; + } + + @Override + + + public boolean showAudioSharingSliderInVolumePanel() { + return false; + } + + @Override + + + public boolean showClipboardIndication() { + return false; + } + + @Override + + + public boolean showLockedByYourWatchKeyguardIndicator() { + return false; + } + + @Override + + + public boolean showToastWhenAppControlBrightness() { + return true; + } + + @Override + + + public boolean simPinBouncerReset() { + return true; + } + + @Override + + + public boolean simPinRaceConditionOnRestart() { + return true; + } + + @Override + + + public boolean simPinUseSlotId() { + return true; + } + + @Override + + + public boolean skipHideSensitiveNotifAnimation() { + return true; + } + + @Override + + public boolean sliceBroadcastRelayInBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return sliceBroadcastRelayInBackground; - + return true; } @Override - + + public boolean sliceManagerBinderCallBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return sliceManagerBinderCallBackground; - + return true; } @Override - + + public boolean smartspaceLockscreenViewmodel() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return smartspaceLockscreenViewmodel; - + return true; } @Override - + + public boolean smartspaceRelocateToBottom() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return smartspaceRelocateToBottom; - + return false; } @Override - - public boolean smartspaceRemoteviewsRendering() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return smartspaceRemoteviewsRendering; + + public boolean smartspaceRemoteviewsRenderingFix() { + return true; } @Override - + + + public boolean smartspaceSwipeEventLoggingFix() { + return true; + } + + @Override + + + public boolean smartspaceViewpager2() { + return false; + } + + @Override + + + public boolean sounddoseCustomization() { + return true; + } + + @Override + + + public boolean spatialModelAppPushback() { + return false; + } + + @Override + + + public boolean stabilizeHeadsUpGroupV2() { + return true; + } + + @Override + + + public boolean statusBarAlwaysCheckUnderlyingNetworks() { + return true; + } + + @Override + + + public boolean statusBarAutoStartScreenRecordChip() { + return true; + } + + @Override + + + public boolean statusBarChipsModernization() { + return false; + } + + @Override + + + public boolean statusBarChipsReturnAnimations() { + return false; + } + + @Override + + + public boolean statusBarFontUpdates() { + return false; + } + + @Override + + + public boolean statusBarMobileIconKairos() { + return false; + } + + @Override + + public boolean statusBarMonochromeIconsFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return statusBarMonochromeIconsFix; - + return true; } @Override - - public boolean statusBarScreenSharingChips() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return statusBarScreenSharingChips; + + public boolean statusBarNoHunBehavior() { + return false; } @Override - + + + public boolean statusBarPopupChips() { + return false; + } + + @Override + + + public boolean statusBarRootModernization() { + return false; + } + + @Override + + + public boolean statusBarShowAudioOnlyProjectionChip() { + return true; + } + + @Override + + + public boolean statusBarSignalPolicyRefactor() { + return true; + } + + @Override + + + public boolean statusBarSignalPolicyRefactorEthernet() { + return true; + } + + @Override + + public boolean statusBarStaticInoutIndicators() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return statusBarStaticInoutIndicators; - + return false; } @Override - + + + public boolean statusBarStopUpdatingWindowHeight() { + return false; + } + + @Override + + + public boolean statusBarSwipeOverChip() { + return false; + } + + @Override + + + public boolean statusBarSwitchToSpnFromDataSpn() { + return true; + } + + @Override + + + public boolean statusBarUiThread() { + return false; + } + + @Override + + + public boolean statusBarWindowNoCustomTouch() { + return false; + } + + @Override + + + public boolean stoppableFgsSystemApp() { + return true; + } + + @Override + + public boolean switchUserOnBg() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return switchUserOnBg; - + return true; } @Override - + + public boolean sysuiTeamfood() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return sysuiTeamfood; - + return false; } @Override - + + public boolean themeOverlayControllerWakefulnessDeprecation() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return themeOverlayControllerWakefulnessDeprecation; - + return false; } @Override - + + + public boolean transitionRaceCondition() { + return true; + } + + @Override + + public boolean translucentOccludingActivityFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return translucentOccludingActivityFix; - + return true; } @Override - - public boolean truncatedStatusBarIconsFix() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return truncatedStatusBarIconsFix; + + public boolean tvGlobalActionsFocus() { + return false; } @Override - + + public boolean udfpsViewPerformance() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return udfpsViewPerformance; - + return true; } @Override - + + public boolean unfoldAnimationBackgroundProgress() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return unfoldAnimationBackgroundProgress; - + return true; } @Override - + + + public boolean unfoldLatencyTrackingFix() { + return false; + } + + @Override + + + public boolean updateCornerRadiusOnDisplayChanged() { + return true; + } + + @Override + + public boolean updateUserSwitcherBackground() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return updateUserSwitcherBackground; - + return true; } @Override - - public boolean validateKeyboardShortcutHelperIconUri() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return validateKeyboardShortcutHelperIconUri; + + public boolean updateWindowMagnifierBottomBoundary() { + return false; } @Override - + + + public boolean useAadProxSensor() { + return false; + } + + @Override + + + public boolean useNotifInflationThreadForFooter() { + return true; + } + + @Override + + + public boolean useNotifInflationThreadForRow() { + return true; + } + + @Override + + + public boolean useTransitionsForKeyguardOccluded() { + return true; + } + + @Override + + + public boolean useVolumeController() { + return true; + } + + @Override + + + public boolean userAwareSettingsRepositories() { + return true; + } + + @Override + + + public boolean userEncryptedSource() { + return true; + } + + @Override + + + public boolean userSwitcherAddSignOutOption() { + return false; + } + + @Override + + public boolean visualInterruptionsRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return visualInterruptionsRefactor; + return true; + } + @Override + + + public boolean volumeRedesign() { + return false; } } - diff --git a/flags/src/com/android/systemui/Flags.java b/flags/src/com/android/systemui/Flags.java index dd9dc1fb73..ca51b086d1 100644 --- a/flags/src/com/android/systemui/Flags.java +++ b/flags/src/com/android/systemui/Flags.java @@ -1,37 +1,74 @@ package com.android.systemui; // TODO(b/303773055): Remove the annotation after access issue is resolved. + /** @hide */ public final class Flags { /** @hide */ public static final String FLAG_ACTIVITY_TRANSITION_USE_LARGEST_WINDOW = "com.android.systemui.activity_transition_use_largest_window"; /** @hide */ + public static final String FLAG_ADD_BLACK_BACKGROUND_FOR_WINDOW_MAGNIFIER = "com.android.systemui.add_black_background_for_window_magnifier"; + /** @hide */ + public static final String FLAG_ALWAYS_COMPOSE_QS_UI_FRAGMENT = "com.android.systemui.always_compose_qs_ui_fragment"; + /** @hide */ public static final String FLAG_AMBIENT_TOUCH_MONITOR_LISTEN_TO_DISPLAY_CHANGES = "com.android.systemui.ambient_touch_monitor_listen_to_display_changes"; /** @hide */ public static final String FLAG_APP_CLIPS_BACKLINKS = "com.android.systemui.app_clips_backlinks"; /** @hide */ + public static final String FLAG_APP_SHORTCUT_REMOVAL_FIX = "com.android.systemui.app_shortcut_removal_fix"; + /** @hide */ + public static final String FLAG_AVALANCHE_REPLACE_HUN_WHEN_CRITICAL = "com.android.systemui.avalanche_replace_hun_when_critical"; + /** @hide */ public static final String FLAG_BIND_KEYGUARD_MEDIA_VISIBILITY = "com.android.systemui.bind_keyguard_media_visibility"; /** @hide */ - public static final String FLAG_BP_TALKBACK = "com.android.systemui.bp_talkback"; + public static final String FLAG_BOUNCER_UI_REVAMP = "com.android.systemui.bouncer_ui_revamp"; + /** @hide */ + public static final String FLAG_BOUNCER_UI_REVAMP_2 = "com.android.systemui.bouncer_ui_revamp_2"; + /** @hide */ + public static final String FLAG_BP_COLORS = "com.android.systemui.bp_colors"; /** @hide */ public static final String FLAG_BRIGHTNESS_SLIDER_FOCUS_STATE = "com.android.systemui.brightness_slider_focus_state"; /** @hide */ - public static final String FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX = "com.android.systemui.centralized_status_bar_height_fix"; + public static final String FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION = "com.android.systemui.check_lockscreen_gone_transition"; + /** @hide */ + public static final String FLAG_CLASSIC_FLAGS_MULTI_USER = "com.android.systemui.classic_flags_multi_user"; + /** @hide */ + public static final String FLAG_CLIPBOARD_IMAGE_TIMEOUT = "com.android.systemui.clipboard_image_timeout"; /** @hide */ public static final String FLAG_CLIPBOARD_NONINTERACTIVE_ON_LOCKSCREEN = "com.android.systemui.clipboard_noninteractive_on_lockscreen"; /** @hide */ - public static final String FLAG_CLOCK_REACTIVE_VARIANTS = "com.android.systemui.clock_reactive_variants"; + public static final String FLAG_CLIPBOARD_OVERLAY_MULTIUSER = "com.android.systemui.clipboard_overlay_multiuser"; + /** @hide */ + public static final String FLAG_CLIPBOARD_SHARED_TRANSITIONS = "com.android.systemui.clipboard_shared_transitions"; + /** @hide */ + public static final String FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE = "com.android.systemui.clipboard_use_description_mimetype"; + /** @hide */ + public static final String FLAG_CLOCK_FIDGET_ANIMATION = "com.android.systemui.clock_fidget_animation"; /** @hide */ public static final String FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN = "com.android.systemui.communal_bouncer_do_not_modify_plugin_open"; /** @hide */ + public static final String FLAG_COMMUNAL_EDIT_WIDGETS_ACTIVITY_FINISH_FIX = "com.android.systemui.communal_edit_widgets_activity_finish_fix"; + /** @hide */ public static final String FLAG_COMMUNAL_HUB = "com.android.systemui.communal_hub"; /** @hide */ + public static final String FLAG_COMMUNAL_HUB_USE_THREAD_POOL_FOR_WIDGETS = "com.android.systemui.communal_hub_use_thread_pool_for_widgets"; + /** @hide */ + public static final String FLAG_COMMUNAL_RESPONSIVE_GRID = "com.android.systemui.communal_responsive_grid"; + /** @hide */ + public static final String FLAG_COMMUNAL_SCENE_KTF_REFACTOR = "com.android.systemui.communal_scene_ktf_refactor"; + /** @hide */ + public static final String FLAG_COMMUNAL_STANDALONE_SUPPORT = "com.android.systemui.communal_standalone_support"; + /** @hide */ + public static final String FLAG_COMMUNAL_TIMER_FLICKER_FIX = "com.android.systemui.communal_timer_flicker_fix"; + /** @hide */ + public static final String FLAG_COMMUNAL_WIDGET_RESIZING = "com.android.systemui.communal_widget_resizing"; + /** @hide */ + public static final String FLAG_COMMUNAL_WIDGET_TRAMPOLINE_FIX = "com.android.systemui.communal_widget_trampoline_fix"; + /** @hide */ public static final String FLAG_COMPOSE_BOUNCER = "com.android.systemui.compose_bouncer"; /** @hide */ - public static final String FLAG_COMPOSE_LOCKSCREEN = "com.android.systemui.compose_lockscreen"; - /** @hide */ public static final String FLAG_CONFINE_NOTIFICATION_TOUCH_TO_VIEW_WIDTH = "com.android.systemui.confine_notification_touch_to_view_width"; /** @hide */ - public static final String FLAG_CONSTRAINT_BP = "com.android.systemui.constraint_bp"; + public static final String FLAG_CONT_AUTH_PLUGIN = "com.android.systemui.cont_auth_plugin"; /** @hide */ public static final String FLAG_CONTEXTUAL_TIPS_ASSISTANT_DISMISS_FIX = "com.android.systemui.contextual_tips_assistant_dismiss_fix"; /** @hide */ @@ -39,25 +76,31 @@ public final class Flags { /** @hide */ public static final String FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER = "com.android.systemui.create_windowless_window_magnifier"; /** @hide */ - public static final String FLAG_DEDICATED_NOTIF_INFLATION_THREAD = "com.android.systemui.dedicated_notif_inflation_thread"; + public static final String FLAG_DEBUG_LIVE_UPDATES_PROMOTE_ALL = "com.android.systemui.debug_live_updates_promote_all"; + /** @hide */ + public static final String FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB = "com.android.systemui.decouple_view_controller_in_animlib"; /** @hide */ public static final String FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON = "com.android.systemui.delay_show_magnification_button"; /** @hide */ - public static final String FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD = "com.android.systemui.delayed_wakelock_release_on_background_thread"; + public static final String FLAG_DESKTOP_EFFECTS_QS_TILE = "com.android.systemui.desktop_effects_qs_tile"; /** @hide */ public static final String FLAG_DEVICE_ENTRY_UDFPS_REFACTOR = "com.android.systemui.device_entry_udfps_refactor"; /** @hide */ + public static final String FLAG_DISABLE_BLURRED_SHADE_VISIBLE = "com.android.systemui.disable_blurred_shade_visible"; + /** @hide */ public static final String FLAG_DISABLE_CONTEXTUAL_TIPS_FREQUENCY_CHECK = "com.android.systemui.disable_contextual_tips_frequency_check"; /** @hide */ public static final String FLAG_DISABLE_CONTEXTUAL_TIPS_IOS_SWITCHER_CHECK = "com.android.systemui.disable_contextual_tips_ios_switcher_check"; /** @hide */ - public static final String FLAG_DOZEUI_SCHEDULING_ALARMS_BACKGROUND_EXECUTION = "com.android.systemui.dozeui_scheduling_alarms_background_execution"; + public static final String FLAG_DISABLE_SHADE_TRACKPAD_TWO_FINGER_SWIPE = "com.android.systemui.disable_shade_trackpad_two_finger_swipe"; + /** @hide */ + public static final String FLAG_DOUBLE_TAP_TO_SLEEP = "com.android.systemui.double_tap_to_sleep"; /** @hide */ public static final String FLAG_DREAM_INPUT_SESSION_PILFER_ONCE = "com.android.systemui.dream_input_session_pilfer_once"; /** @hide */ public static final String FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING = "com.android.systemui.dream_overlay_bouncer_swipe_direction_filtering"; /** @hide */ - public static final String FLAG_DUAL_SHADE = "com.android.systemui.dual_shade"; + public static final String FLAG_DREAM_OVERLAY_UPDATED_FONT = "com.android.systemui.dream_overlay_updated_font"; /** @hide */ public static final String FLAG_EDGE_BACK_GESTURE_HANDLER_THREAD = "com.android.systemui.edge_back_gesture_handler_thread"; /** @hide */ @@ -77,15 +120,29 @@ public final class Flags { /** @hide */ public static final String FLAG_ENABLE_LAYOUT_TRACING = "com.android.systemui.enable_layout_tracing"; /** @hide */ - public static final String FLAG_ENABLE_VIEW_CAPTURE_TRACING = "com.android.systemui.enable_view_capture_tracing"; + public static final String FLAG_ENABLE_UNDERLAY = "com.android.systemui.enable_underlay"; /** @hide */ - public static final String FLAG_ENABLE_WIDGET_PICKER_SIZE_FILTER = "com.android.systemui.enable_widget_picker_size_filter"; + public static final String FLAG_ENABLE_VIEW_CAPTURE_TRACING = "com.android.systemui.enable_view_capture_tracing"; /** @hide */ public static final String FLAG_ENFORCE_BRIGHTNESS_BASE_USER_RESTRICTION = "com.android.systemui.enforce_brightness_base_user_restriction"; /** @hide */ public static final String FLAG_EXAMPLE_FLAG = "com.android.systemui.example_flag"; /** @hide */ - public static final String FLAG_FAST_UNLOCK_TRANSITION = "com.android.systemui.fast_unlock_transition"; + public static final String FLAG_EXPAND_COLLAPSE_PRIVACY_DIALOG = "com.android.systemui.expand_collapse_privacy_dialog"; + /** @hide */ + public static final String FLAG_EXPAND_HEADS_UP_ON_INLINE_REPLY = "com.android.systemui.expand_heads_up_on_inline_reply"; + /** @hide */ + public static final String FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN = "com.android.systemui.expanded_privacy_indicators_on_large_screen"; + /** @hide */ + public static final String FLAG_EXTENDED_APPS_SHORTCUT_CATEGORY = "com.android.systemui.extended_apps_shortcut_category"; + /** @hide */ + public static final String FLAG_FACE_MESSAGE_DEFER_UPDATE = "com.android.systemui.face_message_defer_update"; + /** @hide */ + public static final String FLAG_FACE_SCANNING_ANIMATION_NPE_FIX = "com.android.systemui.face_scanning_animation_npe_fix"; + /** @hide */ + public static final String FLAG_FASTER_UNLOCK_TRANSITION = "com.android.systemui.faster_unlock_transition"; + /** @hide */ + public static final String FLAG_FETCH_BOOKMARKS_XML_KEYBOARD_SHORTCUTS = "com.android.systemui.fetch_bookmarks_xml_keyboard_shortcuts"; /** @hide */ public static final String FLAG_FIX_IMAGE_WALLPAPER_CRASH_SURFACE_ALREADY_RELEASED = "com.android.systemui.fix_image_wallpaper_crash_surface_already_released"; /** @hide */ @@ -93,62 +150,132 @@ public final class Flags { /** @hide */ public static final String FLAG_FLOATING_MENU_ANIMATED_TUCK = "com.android.systemui.floating_menu_animated_tuck"; /** @hide */ + public static final String FLAG_FLOATING_MENU_DISPLAY_CUTOUT_SUPPORT = "com.android.systemui.floating_menu_display_cutout_support"; + /** @hide */ public static final String FLAG_FLOATING_MENU_DRAG_TO_EDIT = "com.android.systemui.floating_menu_drag_to_edit"; /** @hide */ public static final String FLAG_FLOATING_MENU_DRAG_TO_HIDE = "com.android.systemui.floating_menu_drag_to_hide"; /** @hide */ + public static final String FLAG_FLOATING_MENU_HEARING_DEVICE_STATUS_ICON = "com.android.systemui.floating_menu_hearing_device_status_icon"; + /** @hide */ public static final String FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION = "com.android.systemui.floating_menu_ime_displacement_animation"; /** @hide */ public static final String FLAG_FLOATING_MENU_NARROW_TARGET_CONTENT_OBSERVER = "com.android.systemui.floating_menu_narrow_target_content_observer"; /** @hide */ + public static final String FLAG_FLOATING_MENU_NOTIFY_TARGETS_CHANGED_ON_STRICT_DIFF = "com.android.systemui.floating_menu_notify_targets_changed_on_strict_diff"; + /** @hide */ public static final String FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG = "com.android.systemui.floating_menu_overlaps_nav_bars_flag"; /** @hide */ public static final String FLAG_FLOATING_MENU_RADII_ANIMATION = "com.android.systemui.floating_menu_radii_animation"; /** @hide */ - public static final String FLAG_GENERATED_PREVIEWS = "android.appwidget.flags.generated_previews"; - /** @hide */ public static final String FLAG_GET_CONNECTED_DEVICE_NAME_UNSYNCHRONIZED = "com.android.systemui.get_connected_device_name_unsynchronized"; /** @hide */ public static final String FLAG_GLANCEABLE_HUB_ALLOW_KEYGUARD_WHEN_DREAMING = "com.android.systemui.glanceable_hub_allow_keyguard_when_dreaming"; /** @hide */ - public static final String FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE = "com.android.systemui.glanceable_hub_fullscreen_swipe"; + public static final String FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND = "com.android.systemui.glanceable_hub_blurred_background"; /** @hide */ - public static final String FLAG_GLANCEABLE_HUB_GESTURE_HANDLE = "com.android.systemui.glanceable_hub_gesture_handle"; + public static final String FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE = "com.android.systemui.glanceable_hub_direct_edit_mode"; /** @hide */ - public static final String FLAG_GLANCEABLE_HUB_SHORTCUT_BUTTON = "com.android.systemui.glanceable_hub_shortcut_button"; + public static final String FLAG_GLANCEABLE_HUB_V2 = "com.android.systemui.glanceable_hub_v2"; /** @hide */ - public static final String FLAG_HAPTIC_BRIGHTNESS_SLIDER = "com.android.systemui.haptic_brightness_slider"; + public static final String FLAG_GLANCEABLE_HUB_V2_RESOURCES = "com.android.systemui.glanceable_hub_v2_resources"; /** @hide */ - public static final String FLAG_HAPTIC_VOLUME_SLIDER = "com.android.systemui.haptic_volume_slider"; + public static final String FLAG_HAPTICS_FOR_COMPOSE_SLIDERS = "com.android.systemui.haptics_for_compose_sliders"; + /** @hide */ + public static final String FLAG_HARDWARE_COLOR_STYLES = "com.android.systemui.hardware_color_styles"; /** @hide */ public static final String FLAG_HEARING_AIDS_QS_TILE_DIALOG = "com.android.systemui.hearing_aids_qs_tile_dialog"; /** @hide */ public static final String FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS = "com.android.systemui.hearing_devices_dialog_related_tools"; /** @hide */ + public static final String FLAG_HIDE_RINGER_BUTTON_IN_SINGLE_VOLUME_MODE = "com.android.systemui.hide_ringer_button_in_single_volume_mode"; + /** @hide */ + public static final String FLAG_HOME_CONTROLS_DREAM_HSUM = "com.android.systemui.home_controls_dream_hsum"; + /** @hide */ + public static final String FLAG_HUB_EDIT_MODE_TOUCH_ADJUSTMENTS = "com.android.systemui.hub_edit_mode_touch_adjustments"; + /** @hide */ + public static final String FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE = "com.android.systemui.hubmode_fullscreen_vertical_swipe"; + /** @hide */ + public static final String FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX = "com.android.systemui.hubmode_fullscreen_vertical_swipe_fix"; + /** @hide */ + public static final String FLAG_ICON_REFRESH_2025 = "com.android.systemui.icon_refresh_2025"; + /** @hide */ + public static final String FLAG_IGNORE_TOUCHES_NEXT_TO_NOTIFICATION_SHELF = "com.android.systemui.ignore_touches_next_to_notification_shelf"; + /** @hide */ + public static final String FLAG_INDICATION_TEXT_A11Y_FIX = "com.android.systemui.indication_text_a11y_fix"; + /** @hide */ public static final String FLAG_KEYBOARD_DOCKING_INDICATOR = "com.android.systemui.keyboard_docking_indicator"; /** @hide */ public static final String FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE = "com.android.systemui.keyboard_shortcut_helper_rewrite"; /** @hide */ - public static final String FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR = "com.android.systemui.keyguard_bottom_area_refactor"; + public static final String FLAG_KEYBOARD_SHORTCUT_HELPER_SHORTCUT_CUSTOMIZER = "com.android.systemui.keyboard_shortcut_helper_shortcut_customizer"; + /** @hide */ + public static final String FLAG_KEYBOARD_TOUCHPAD_CONTEXTUAL_EDUCATION = "com.android.systemui.keyboard_touchpad_contextual_education"; + /** @hide */ + public static final String FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF = "com.android.systemui.keyguard_transition_force_finish_on_screen_off"; + /** @hide */ + public static final String FLAG_KEYGUARD_WM_REORDER_ATMS_CALLS = "com.android.systemui.keyguard_wm_reorder_atms_calls"; /** @hide */ public static final String FLAG_KEYGUARD_WM_STATE_REFACTOR = "com.android.systemui.keyguard_wm_state_refactor"; /** @hide */ - public static final String FLAG_LIGHT_REVEAL_MIGRATION = "com.android.systemui.light_reveal_migration"; + public static final String FLAG_LOCKSCREEN_FONT = "com.android.systemui.lockscreen_font"; + /** @hide */ + public static final String FLAG_LOW_LIGHT_CLOCK_DREAM = "com.android.systemui.low_light_clock_dream"; + /** @hide */ + public static final String FLAG_MAGNETIC_NOTIFICATION_SWIPES = "com.android.systemui.magnetic_notification_swipes"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_A11Y_COLORS = "com.android.systemui.media_controls_a11y_colors"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3 = "com.android.systemui.media_controls_button_media3"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3_PLACEMENT = "com.android.systemui.media_controls_button_media3_placement"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION = "com.android.systemui.media_controls_device_manager_background_execution"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE_BUGFIX = "com.android.systemui.media_controls_drawables_reuse_bugfix"; /** @hide */ public static final String FLAG_MEDIA_CONTROLS_LOCKSCREEN_SHADE_BUG_FIX = "com.android.systemui.media_controls_lockscreen_shade_bug_fix"; /** @hide */ - public static final String FLAG_MEDIA_CONTROLS_REFACTOR = "com.android.systemui.media_controls_refactor"; + public static final String FLAG_MEDIA_CONTROLS_UI_UPDATE = "com.android.systemui.media_controls_ui_update"; + /** @hide */ + public static final String FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND = "com.android.systemui.media_controls_umo_inflation_in_background"; /** @hide */ public static final String FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT = "com.android.systemui.media_controls_user_initiated_deleteintent"; /** @hide */ - public static final String FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT = "com.android.systemui.migrate_clocks_to_blueprint"; + public static final String FLAG_MEDIA_LOAD_METADATA_VIA_MEDIA_DATA_LOADER = "com.android.systemui.media_load_metadata_via_media_data_loader"; + /** @hide */ + public static final String FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION = "com.android.systemui.media_lockscreen_launch_animation"; + /** @hide */ + public static final String FLAG_MEDIA_PROJECTION_DIALOG_BEHIND_LOCKSCREEN = "com.android.systemui.media_projection_dialog_behind_lockscreen"; + /** @hide */ + public static final String FLAG_MEDIA_PROJECTION_GREY_ERROR_TEXT = "com.android.systemui.media_projection_grey_error_text"; + /** @hide */ + public static final String FLAG_MEDIA_PROJECTION_REQUEST_ATTRIBUTION_FIX = "com.android.systemui.media_projection_request_attribution_fix"; + /** @hide */ + public static final String FLAG_MODES_UI_DIALOG_PAGING = "com.android.systemui.modes_ui_dialog_paging"; + /** @hide */ + public static final String FLAG_MOVE_TRANSITION_ANIMATION_LAYER = "com.android.systemui.move_transition_animation_layer"; + /** @hide */ + public static final String FLAG_MSDL_FEEDBACK = "com.android.systemui.msdl_feedback"; + /** @hide */ + public static final String FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT = "com.android.systemui.multiuser_wifi_picker_tracker_support"; /** @hide */ public static final String FLAG_NEW_AOD_TRANSITION = "com.android.systemui.new_aod_transition"; /** @hide */ - public static final String FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL = "com.android.systemui.new_touchpad_gestures_tutorial"; - /** @hide */ public static final String FLAG_NEW_VOLUME_PANEL = "com.android.systemui.new_volume_panel"; /** @hide */ + public static final String FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING = "com.android.systemui.non_touchscreen_devices_bypass_falsing"; + /** @hide */ + public static final String FLAG_NOTES_ROLE_QS_TILE = "com.android.systemui.notes_role_qs_tile"; + /** @hide */ + public static final String FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS = "com.android.systemui.notification_add_x_on_hover_to_dismiss"; + /** @hide */ + public static final String FLAG_NOTIFICATION_AMBIENT_SUPPRESSION_AFTER_INFLATION = "com.android.systemui.notification_ambient_suppression_after_inflation"; + /** @hide */ + public static final String FLAG_NOTIFICATION_ANIMATED_ACTIONS_TREATMENT = "com.android.systemui.notification_animated_actions_treatment"; + /** @hide */ + public static final String FLAG_NOTIFICATION_APPEAR_NONLINEAR = "com.android.systemui.notification_appear_nonlinear"; + /** @hide */ public static final String FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION = "com.android.systemui.notification_async_group_header_inflation"; /** @hide */ public static final String FLAG_NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION = "com.android.systemui.notification_async_hybrid_view_inflation"; @@ -159,57 +286,81 @@ public final class Flags { /** @hide */ public static final String FLAG_NOTIFICATION_BACKGROUND_TINT_OPTIMIZATION = "com.android.systemui.notification_background_tint_optimization"; /** @hide */ + public static final String FLAG_NOTIFICATION_BUNDLE_UI = "com.android.systemui.notification_bundle_ui"; + /** @hide */ public static final String FLAG_NOTIFICATION_COLOR_UPDATE_LOGGER = "com.android.systemui.notification_color_update_logger"; /** @hide */ public static final String FLAG_NOTIFICATION_CONTENT_ALPHA_OPTIMIZATION = "com.android.systemui.notification_content_alpha_optimization"; /** @hide */ public static final String FLAG_NOTIFICATION_FOOTER_BACKGROUND_TINT_OPTIMIZATION = "com.android.systemui.notification_footer_background_tint_optimization"; /** @hide */ - public static final String FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION = "com.android.systemui.notification_media_manager_background_execution"; - /** @hide */ - public static final String FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE = "com.android.systemui.notification_minimalism_prototype"; - /** @hide */ public static final String FLAG_NOTIFICATION_OVER_EXPANSION_CLIPPING_FIX = "com.android.systemui.notification_over_expansion_clipping_fix"; /** @hide */ - public static final String FLAG_NOTIFICATION_PULSING_FIX = "com.android.systemui.notification_pulsing_fix"; + public static final String FLAG_NOTIFICATION_REENTRANT_DISMISS = "com.android.systemui.notification_reentrant_dismiss"; + /** @hide */ + public static final String FLAG_NOTIFICATION_ROW_ACCESSIBILITY_EXPANDED = "com.android.systemui.notification_row_accessibility_expanded"; /** @hide */ public static final String FLAG_NOTIFICATION_ROW_CONTENT_BINDER_REFACTOR = "com.android.systemui.notification_row_content_binder_refactor"; /** @hide */ + public static final String FLAG_NOTIFICATION_ROW_TRANSPARENCY = "com.android.systemui.notification_row_transparency"; + /** @hide */ public static final String FLAG_NOTIFICATION_ROW_USER_CONTEXT = "com.android.systemui.notification_row_user_context"; /** @hide */ + public static final String FLAG_NOTIFICATION_SHADE_BLUR = "com.android.systemui.notification_shade_blur"; + /** @hide */ + public static final String FLAG_NOTIFICATION_SHADE_UI_THREAD = "com.android.systemui.notification_shade_ui_thread"; + /** @hide */ + public static final String FLAG_NOTIFICATION_SKIP_SILENT_UPDATES = "com.android.systemui.notification_skip_silent_updates"; + /** @hide */ + public static final String FLAG_NOTIFICATION_TRANSPARENT_HEADER_FIX = "com.android.systemui.notification_transparent_header_fix"; + /** @hide */ public static final String FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING_V2 = "com.android.systemui.notification_view_flipper_pausing_v2"; /** @hide */ public static final String FLAG_NOTIFICATIONS_BACKGROUND_ICONS = "com.android.systemui.notifications_background_icons"; /** @hide */ - public static final String FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR = "com.android.systemui.notifications_footer_view_refactor"; - /** @hide */ - public static final String FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR = "com.android.systemui.notifications_heads_up_refactor"; + public static final String FLAG_NOTIFICATIONS_FOOTER_VISIBILITY_FIX = "com.android.systemui.notifications_footer_visibility_fix"; /** @hide */ public static final String FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH = "com.android.systemui.notifications_hide_on_display_switch"; /** @hide */ + public static final String FLAG_NOTIFICATIONS_HUN_SHARED_ANIMATION_VALUES = "com.android.systemui.notifications_hun_shared_animation_values"; + /** @hide */ public static final String FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR = "com.android.systemui.notifications_icon_container_refactor"; /** @hide */ - public static final String FLAG_NOTIFICATIONS_IMPROVED_HUN_ANIMATION = "com.android.systemui.notifications_improved_hun_animation"; + public static final String FLAG_NOTIFICATIONS_LAUNCH_RADIUS = "com.android.systemui.notifications_launch_radius"; /** @hide */ public static final String FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR = "com.android.systemui.notifications_live_data_store_refactor"; /** @hide */ + public static final String FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE = "com.android.systemui.notifications_pinned_hun_in_shade"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW = "com.android.systemui.notifications_redesign_footer_view"; + /** @hide */ + public static final String FLAG_NOTIFICATIONS_REDESIGN_GUTS = "com.android.systemui.notifications_redesign_guts"; + /** @hide */ + public static final String FLAG_NOTIFY_PASSWORD_TEXT_VIEW_USER_ACTIVITY_IN_BACKGROUND = "com.android.systemui.notify_password_text_view_user_activity_in_background"; + /** @hide */ public static final String FLAG_NOTIFY_POWER_MANAGER_USER_ACTIVITY_BACKGROUND = "com.android.systemui.notify_power_manager_user_activity_background"; /** @hide */ + public static final String FLAG_ONLY_SHOW_MEDIA_STREAM_SLIDER_IN_SINGLE_VOLUME_MODE = "com.android.systemui.only_show_media_stream_slider_in_single_volume_mode"; + /** @hide */ + public static final String FLAG_OUTPUT_SWITCHER_REDESIGN = "com.android.systemui.output_switcher_redesign"; + /** @hide */ + public static final String FLAG_OVERRIDE_SUPPRESS_OVERLAY_CONDITION = "com.android.systemui.override_suppress_overlay_condition"; + /** @hide */ + public static final String FLAG_PERMISSION_HELPER_INLINE_UI_RICH_ONGOING = "com.android.systemui.permission_helper_inline_ui_rich_ongoing"; + /** @hide */ + public static final String FLAG_PERMISSION_HELPER_UI_RICH_ONGOING = "com.android.systemui.permission_helper_ui_rich_ongoing"; + /** @hide */ + public static final String FLAG_PHYSICAL_NOTIFICATION_MOVEMENT = "com.android.systemui.physical_notification_movement"; + /** @hide */ public static final String FLAG_PIN_INPUT_FIELD_STYLED_FOCUS_STATE = "com.android.systemui.pin_input_field_styled_focus_state"; /** @hide */ - public static final String FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER = "com.android.systemui.predictive_back_animate_bouncer"; - /** @hide */ - public static final String FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS = "com.android.systemui.predictive_back_animate_dialogs"; - /** @hide */ public static final String FLAG_PREDICTIVE_BACK_ANIMATE_SHADE = "com.android.systemui.predictive_back_animate_shade"; /** @hide */ - public static final String FLAG_PREDICTIVE_BACK_SYSUI = "com.android.systemui.predictive_back_sysui"; + public static final String FLAG_PREDICTIVE_BACK_DELAY_WM_TRANSITION = "com.android.systemui.predictive_back_delay_wm_transition"; /** @hide */ public static final String FLAG_PRIORITY_PEOPLE_SECTION = "com.android.systemui.priority_people_section"; /** @hide */ - public static final String FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX = "com.android.systemui.privacy_dot_unfold_wrong_corner_fix"; - /** @hide */ - public static final String FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX = "com.android.systemui.pss_app_selector_abrupt_exit_fix"; + public static final String FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY = "com.android.systemui.promote_notifications_automatically"; /** @hide */ public static final String FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN = "com.android.systemui.pss_app_selector_recents_split_screen"; /** @hide */ @@ -217,32 +368,42 @@ public final class Flags { /** @hide */ public static final String FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX = "com.android.systemui.qs_custom_tile_click_guaranteed_bug_fix"; /** @hide */ - public static final String FLAG_QS_NEW_PIPELINE = "com.android.systemui.qs_new_pipeline"; - /** @hide */ public static final String FLAG_QS_NEW_TILES = "com.android.systemui.qs_new_tiles"; /** @hide */ public static final String FLAG_QS_NEW_TILES_FUTURE = "com.android.systemui.qs_new_tiles_future"; /** @hide */ + public static final String FLAG_QS_QUICK_REBIND_ACTIVE_TILES = "com.android.systemui.qs_quick_rebind_active_tiles"; + /** @hide */ + public static final String FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD = "com.android.systemui.qs_register_setting_observer_on_bg_thread"; + /** @hide */ + public static final String FLAG_QS_TILE_DETAILED_VIEW = "com.android.systemui.qs_tile_detailed_view"; + /** @hide */ public static final String FLAG_QS_TILE_FOCUS_STATE = "com.android.systemui.qs_tile_focus_state"; /** @hide */ public static final String FLAG_QS_UI_REFACTOR = "com.android.systemui.qs_ui_refactor"; /** @hide */ - public static final String FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS = "com.android.systemui.quick_settings_visual_haptics_longpress"; + public static final String FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT = "com.android.systemui.qs_ui_refactor_compose_fragment"; /** @hide */ public static final String FLAG_RECORD_ISSUE_QS_TILE = "com.android.systemui.record_issue_qs_tile"; /** @hide */ + public static final String FLAG_REDESIGN_MAGNIFICATION_WINDOW_SIZE = "com.android.systemui.redesign_magnification_window_size"; + /** @hide */ public static final String FLAG_REFACTOR_GET_CURRENT_USER = "com.android.systemui.refactor_get_current_user"; /** @hide */ public static final String FLAG_REGISTER_BATTERY_CONTROLLER_RECEIVERS_IN_CORESTARTABLE = "com.android.systemui.register_battery_controller_receivers_in_corestartable"; /** @hide */ + public static final String FLAG_REGISTER_CONTENT_OBSERVERS_ASYNC = "com.android.systemui.register_content_observers_async"; + /** @hide */ public static final String FLAG_REGISTER_NEW_WALLET_CARD_IN_BACKGROUND = "com.android.systemui.register_new_wallet_card_in_background"; /** @hide */ public static final String FLAG_REGISTER_WALLPAPER_NOTIFIER_BACKGROUND = "com.android.systemui.register_wallpaper_notifier_background"; /** @hide */ - public static final String FLAG_REGISTER_ZEN_MODE_CONTENT_OBSERVER_BACKGROUND = "com.android.systemui.register_zen_mode_content_observer_background"; + public static final String FLAG_RELOCK_WITH_POWER_BUTTON_IMMEDIATELY = "com.android.systemui.relock_with_power_button_immediately"; /** @hide */ public static final String FLAG_REMOVE_DREAM_OVERLAY_HIDE_ON_TOUCH = "com.android.systemui.remove_dream_overlay_hide_on_touch"; /** @hide */ + public static final String FLAG_REMOVE_UPDATE_LISTENER_IN_QS_ICON_VIEW_IMPL = "com.android.systemui.remove_update_listener_in_qs_icon_view_impl"; + /** @hide */ public static final String FLAG_REST_TO_UNLOCK = "com.android.systemui.rest_to_unlock"; /** @hide */ public static final String FLAG_RESTART_DREAM_ON_UNOCCLUDE = "com.android.systemui.restart_dream_on_unocclude"; @@ -259,18 +420,46 @@ public final class Flags { /** @hide */ public static final String FLAG_SCREENSHOT_ACTION_DISMISS_SYSTEM_WINDOWS = "com.android.systemui.screenshot_action_dismiss_system_windows"; /** @hide */ - public static final String FLAG_SCREENSHOT_PRIVATE_PROFILE_ACCESSIBILITY_ANNOUNCEMENT_FIX = "com.android.systemui.screenshot_private_profile_accessibility_announcement_fix"; + public static final String FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE = "com.android.systemui.screenshot_multidisplay_focus_change"; /** @hide */ - public static final String FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX = "com.android.systemui.screenshot_private_profile_behavior_fix"; + public static final String FLAG_SCREENSHOT_POLICY_SPLIT_AND_DESKTOP_MODE = "com.android.systemui.screenshot_policy_split_and_desktop_mode"; /** @hide */ public static final String FLAG_SCREENSHOT_SCROLL_CROP_VIEW_CRASH_FIX = "com.android.systemui.screenshot_scroll_crop_view_crash_fix"; /** @hide */ - public static final String FLAG_SCREENSHOT_SHELF_UI2 = "com.android.systemui.screenshot_shelf_ui2"; + public static final String FLAG_SCREENSHOT_UI_CONTROLLER_REFACTOR = "com.android.systemui.screenshot_ui_controller_refactor"; /** @hide */ - public static final String FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX = "com.android.systemui.shade_collapse_activity_launch_fix"; + public static final String FLAG_SECONDARY_USER_WIDGET_HOST = "com.android.systemui.secondary_user_widget_host"; + /** @hide */ + public static final String FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD = "com.android.systemui.settings_ext_register_content_observer_on_bg_thread"; + /** @hide */ + public static final String FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS = "com.android.systemui.shade_expands_on_status_bar_long_press"; + /** @hide */ + public static final String FLAG_SHADE_HEADER_FONT_UPDATE = "com.android.systemui.shade_header_font_update"; + /** @hide */ + public static final String FLAG_SHADE_LAUNCH_ACCESSIBILITY = "com.android.systemui.shade_launch_accessibility"; + /** @hide */ + public static final String FLAG_SHADE_WINDOW_GOES_AROUND = "com.android.systemui.shade_window_goes_around"; /** @hide */ public static final String FLAG_SHADERLIB_LOADING_EFFECT_REFACTOR = "com.android.systemui.shaderlib_loading_effect_refactor"; /** @hide */ + public static final String FLAG_SHORTCUT_HELPER_KEY_GLYPH = "com.android.systemui.shortcut_helper_key_glyph"; + /** @hide */ + public static final String FLAG_SHOW_AUDIO_SHARING_SLIDER_IN_VOLUME_PANEL = "com.android.systemui.show_audio_sharing_slider_in_volume_panel"; + /** @hide */ + public static final String FLAG_SHOW_CLIPBOARD_INDICATION = "com.android.systemui.show_clipboard_indication"; + /** @hide */ + public static final String FLAG_SHOW_LOCKED_BY_YOUR_WATCH_KEYGUARD_INDICATOR = "com.android.systemui.show_locked_by_your_watch_keyguard_indicator"; + /** @hide */ + public static final String FLAG_SHOW_TOAST_WHEN_APP_CONTROL_BRIGHTNESS = "com.android.systemui.show_toast_when_app_control_brightness"; + /** @hide */ + public static final String FLAG_SIM_PIN_BOUNCER_RESET = "com.android.systemui.sim_pin_bouncer_reset"; + /** @hide */ + public static final String FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART = "com.android.systemui.sim_pin_race_condition_on_restart"; + /** @hide */ + public static final String FLAG_SIM_PIN_USE_SLOT_ID = "com.android.systemui.sim_pin_use_slot_id"; + /** @hide */ + public static final String FLAG_SKIP_HIDE_SENSITIVE_NOTIF_ANIMATION = "com.android.systemui.skip_hide_sensitive_notif_animation"; + /** @hide */ public static final String FLAG_SLICE_BROADCAST_RELAY_IN_BACKGROUND = "com.android.systemui.slice_broadcast_relay_in_background"; /** @hide */ public static final String FLAG_SLICE_MANAGER_BINDER_CALL_BACKGROUND = "com.android.systemui.slice_manager_binder_call_background"; @@ -279,642 +468,2062 @@ public final class Flags { /** @hide */ public static final String FLAG_SMARTSPACE_RELOCATE_TO_BOTTOM = "com.android.systemui.smartspace_relocate_to_bottom"; /** @hide */ - public static final String FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING = "com.android.systemui.smartspace_remoteviews_rendering"; + public static final String FLAG_SMARTSPACE_REMOTEVIEWS_RENDERING_FIX = "com.android.systemui.smartspace_remoteviews_rendering_fix"; + /** @hide */ + public static final String FLAG_SMARTSPACE_SWIPE_EVENT_LOGGING_FIX = "com.android.systemui.smartspace_swipe_event_logging_fix"; + /** @hide */ + public static final String FLAG_SMARTSPACE_VIEWPAGER2 = "com.android.systemui.smartspace_viewpager2"; + /** @hide */ + public static final String FLAG_SOUNDDOSE_CUSTOMIZATION = "com.android.systemui.sounddose_customization"; + /** @hide */ + public static final String FLAG_SPATIAL_MODEL_APP_PUSHBACK = "com.android.systemui.spatial_model_app_pushback"; + /** @hide */ + public static final String FLAG_STABILIZE_HEADS_UP_GROUP_V2 = "com.android.systemui.stabilize_heads_up_group_v2"; + /** @hide */ + public static final String FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS = "com.android.systemui.status_bar_always_check_underlying_networks"; + /** @hide */ + public static final String FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP = "com.android.systemui.status_bar_auto_start_screen_record_chip"; + /** @hide */ + public static final String FLAG_STATUS_BAR_CHIPS_MODERNIZATION = "com.android.systemui.status_bar_chips_modernization"; + /** @hide */ + public static final String FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS = "com.android.systemui.status_bar_chips_return_animations"; + /** @hide */ + public static final String FLAG_STATUS_BAR_FONT_UPDATES = "com.android.systemui.status_bar_font_updates"; + /** @hide */ + public static final String FLAG_STATUS_BAR_MOBILE_ICON_KAIROS = "com.android.systemui.status_bar_mobile_icon_kairos"; /** @hide */ public static final String FLAG_STATUS_BAR_MONOCHROME_ICONS_FIX = "com.android.systemui.status_bar_monochrome_icons_fix"; /** @hide */ - public static final String FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS = "com.android.systemui.status_bar_screen_sharing_chips"; + public static final String FLAG_STATUS_BAR_NO_HUN_BEHAVIOR = "com.android.systemui.status_bar_no_hun_behavior"; + /** @hide */ + public static final String FLAG_STATUS_BAR_POPUP_CHIPS = "com.android.systemui.status_bar_popup_chips"; + /** @hide */ + public static final String FLAG_STATUS_BAR_ROOT_MODERNIZATION = "com.android.systemui.status_bar_root_modernization"; + /** @hide */ + public static final String FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP = "com.android.systemui.status_bar_show_audio_only_projection_chip"; + /** @hide */ + public static final String FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR = "com.android.systemui.status_bar_signal_policy_refactor"; + /** @hide */ + public static final String FLAG_STATUS_BAR_SIGNAL_POLICY_REFACTOR_ETHERNET = "com.android.systemui.status_bar_signal_policy_refactor_ethernet"; /** @hide */ public static final String FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS = "com.android.systemui.status_bar_static_inout_indicators"; /** @hide */ + public static final String FLAG_STATUS_BAR_STOP_UPDATING_WINDOW_HEIGHT = "com.android.systemui.status_bar_stop_updating_window_height"; + /** @hide */ + public static final String FLAG_STATUS_BAR_SWIPE_OVER_CHIP = "com.android.systemui.status_bar_swipe_over_chip"; + /** @hide */ + public static final String FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN = "com.android.systemui.status_bar_switch_to_spn_from_data_spn"; + /** @hide */ + public static final String FLAG_STATUS_BAR_UI_THREAD = "com.android.systemui.status_bar_ui_thread"; + /** @hide */ + public static final String FLAG_STATUS_BAR_WINDOW_NO_CUSTOM_TOUCH = "com.android.systemui.status_bar_window_no_custom_touch"; + /** @hide */ + public static final String FLAG_STOPPABLE_FGS_SYSTEM_APP = "com.android.systemui.stoppable_fgs_system_app"; + /** @hide */ public static final String FLAG_SWITCH_USER_ON_BG = "com.android.systemui.switch_user_on_bg"; /** @hide */ public static final String FLAG_SYSUI_TEAMFOOD = "com.android.systemui.sysui_teamfood"; /** @hide */ public static final String FLAG_THEME_OVERLAY_CONTROLLER_WAKEFULNESS_DEPRECATION = "com.android.systemui.theme_overlay_controller_wakefulness_deprecation"; /** @hide */ + public static final String FLAG_TRANSITION_RACE_CONDITION = "com.android.systemui.transition_race_condition"; + /** @hide */ public static final String FLAG_TRANSLUCENT_OCCLUDING_ACTIVITY_FIX = "com.android.systemui.translucent_occluding_activity_fix"; /** @hide */ - public static final String FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX = "com.android.systemui.truncated_status_bar_icons_fix"; + public static final String FLAG_TV_GLOBAL_ACTIONS_FOCUS = "com.android.systemui.tv_global_actions_focus"; /** @hide */ public static final String FLAG_UDFPS_VIEW_PERFORMANCE = "com.android.systemui.udfps_view_performance"; /** @hide */ public static final String FLAG_UNFOLD_ANIMATION_BACKGROUND_PROGRESS = "com.android.systemui.unfold_animation_background_progress"; /** @hide */ + public static final String FLAG_UNFOLD_LATENCY_TRACKING_FIX = "com.android.systemui.unfold_latency_tracking_fix"; + /** @hide */ + public static final String FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED = "com.android.systemui.update_corner_radius_on_display_changed"; + /** @hide */ public static final String FLAG_UPDATE_USER_SWITCHER_BACKGROUND = "com.android.systemui.update_user_switcher_background"; /** @hide */ - public static final String FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI = "com.android.systemui.validate_keyboard_shortcut_helper_icon_uri"; + public static final String FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY = "com.android.systemui.update_window_magnifier_bottom_boundary"; + /** @hide */ + public static final String FLAG_USE_AAD_PROX_SENSOR = "com.android.systemui.use_aad_prox_sensor"; + /** @hide */ + public static final String FLAG_USE_NOTIF_INFLATION_THREAD_FOR_FOOTER = "com.android.systemui.use_notif_inflation_thread_for_footer"; + /** @hide */ + public static final String FLAG_USE_NOTIF_INFLATION_THREAD_FOR_ROW = "com.android.systemui.use_notif_inflation_thread_for_row"; + /** @hide */ + public static final String FLAG_USE_TRANSITIONS_FOR_KEYGUARD_OCCLUDED = "com.android.systemui.use_transitions_for_keyguard_occluded"; + /** @hide */ + public static final String FLAG_USE_VOLUME_CONTROLLER = "com.android.systemui.use_volume_controller"; + /** @hide */ + public static final String FLAG_USER_AWARE_SETTINGS_REPOSITORIES = "com.android.systemui.user_aware_settings_repositories"; + /** @hide */ + public static final String FLAG_USER_ENCRYPTED_SOURCE = "com.android.systemui.user_encrypted_source"; + /** @hide */ + public static final String FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION = "com.android.systemui.user_switcher_add_sign_out_option"; /** @hide */ public static final String FLAG_VISUAL_INTERRUPTIONS_REFACTOR = "com.android.systemui.visual_interruptions_refactor"; - + /** @hide */ + public static final String FLAG_VOLUME_REDESIGN = "com.android.systemui.volume_redesign"; + + + public static boolean activityTransitionUseLargestWindow() { + return FEATURE_FLAGS.activityTransitionUseLargestWindow(); } - + + + + public static boolean addBlackBackgroundForWindowMagnifier() { + + return FEATURE_FLAGS.addBlackBackgroundForWindowMagnifier(); + } + + + + public static boolean alwaysComposeQsUiFragment() { + + return FEATURE_FLAGS.alwaysComposeQsUiFragment(); + } + + + public static boolean ambientTouchMonitorListenToDisplayChanges() { + return FEATURE_FLAGS.ambientTouchMonitorListenToDisplayChanges(); } - + + + public static boolean appClipsBacklinks() { + return FEATURE_FLAGS.appClipsBacklinks(); } - + + + + public static boolean appShortcutRemovalFix() { + + return FEATURE_FLAGS.appShortcutRemovalFix(); + } + + + + public static boolean avalancheReplaceHunWhenCritical() { + + return FEATURE_FLAGS.avalancheReplaceHunWhenCritical(); + } + + + public static boolean bindKeyguardMediaVisibility() { + return FEATURE_FLAGS.bindKeyguardMediaVisibility(); } - - public static boolean bpTalkback() { - return FEATURE_FLAGS.bpTalkback(); + + + + public static boolean bouncerUiRevamp() { + + return FEATURE_FLAGS.bouncerUiRevamp(); } - + + + + public static boolean bouncerUiRevamp2() { + + return FEATURE_FLAGS.bouncerUiRevamp2(); + } + + + + public static boolean bpColors() { + + return FEATURE_FLAGS.bpColors(); + } + + + public static boolean brightnessSliderFocusState() { + return FEATURE_FLAGS.brightnessSliderFocusState(); } - - public static boolean centralizedStatusBarHeightFix() { - return FEATURE_FLAGS.centralizedStatusBarHeightFix(); + + + + public static boolean checkLockscreenGoneTransition() { + + return FEATURE_FLAGS.checkLockscreenGoneTransition(); } - + + + + public static boolean classicFlagsMultiUser() { + + return FEATURE_FLAGS.classicFlagsMultiUser(); + } + + + + public static boolean clipboardImageTimeout() { + + return FEATURE_FLAGS.clipboardImageTimeout(); + } + + + public static boolean clipboardNoninteractiveOnLockscreen() { + return FEATURE_FLAGS.clipboardNoninteractiveOnLockscreen(); } - - public static boolean clockReactiveVariants() { - return FEATURE_FLAGS.clockReactiveVariants(); + + + + public static boolean clipboardOverlayMultiuser() { + + return FEATURE_FLAGS.clipboardOverlayMultiuser(); } - + + + + public static boolean clipboardSharedTransitions() { + + return FEATURE_FLAGS.clipboardSharedTransitions(); + } + + + + public static boolean clipboardUseDescriptionMimetype() { + + return FEATURE_FLAGS.clipboardUseDescriptionMimetype(); + } + + + + public static boolean clockFidgetAnimation() { + + return FEATURE_FLAGS.clockFidgetAnimation(); + } + + + public static boolean communalBouncerDoNotModifyPluginOpen() { + return FEATURE_FLAGS.communalBouncerDoNotModifyPluginOpen(); } - + + + + public static boolean communalEditWidgetsActivityFinishFix() { + + return FEATURE_FLAGS.communalEditWidgetsActivityFinishFix(); + } + + + public static boolean communalHub() { + return FEATURE_FLAGS.communalHub(); } - + + + + public static boolean communalHubUseThreadPoolForWidgets() { + + return FEATURE_FLAGS.communalHubUseThreadPoolForWidgets(); + } + + + + public static boolean communalResponsiveGrid() { + + return FEATURE_FLAGS.communalResponsiveGrid(); + } + + + + public static boolean communalSceneKtfRefactor() { + + return FEATURE_FLAGS.communalSceneKtfRefactor(); + } + + + + public static boolean communalStandaloneSupport() { + + return FEATURE_FLAGS.communalStandaloneSupport(); + } + + + + public static boolean communalTimerFlickerFix() { + + return FEATURE_FLAGS.communalTimerFlickerFix(); + } + + + + public static boolean communalWidgetResizing() { + + return FEATURE_FLAGS.communalWidgetResizing(); + } + + + + public static boolean communalWidgetTrampolineFix() { + + return FEATURE_FLAGS.communalWidgetTrampolineFix(); + } + + + public static boolean composeBouncer() { + return FEATURE_FLAGS.composeBouncer(); } - - public static boolean composeLockscreen() { - return FEATURE_FLAGS.composeLockscreen(); - } - + + + public static boolean confineNotificationTouchToViewWidth() { + return FEATURE_FLAGS.confineNotificationTouchToViewWidth(); } - - public static boolean constraintBp() { - return FEATURE_FLAGS.constraintBp(); + + + + public static boolean contAuthPlugin() { + + return FEATURE_FLAGS.contAuthPlugin(); } - + + + public static boolean contextualTipsAssistantDismissFix() { + return FEATURE_FLAGS.contextualTipsAssistantDismissFix(); } - + + + public static boolean coroutineTracing() { + return FEATURE_FLAGS.coroutineTracing(); } - + + + public static boolean createWindowlessWindowMagnifier() { + return FEATURE_FLAGS.createWindowlessWindowMagnifier(); } - - public static boolean dedicatedNotifInflationThread() { - return FEATURE_FLAGS.dedicatedNotifInflationThread(); + + + + public static boolean debugLiveUpdatesPromoteAll() { + + return FEATURE_FLAGS.debugLiveUpdatesPromoteAll(); } - + + + + public static boolean decoupleViewControllerInAnimlib() { + + return FEATURE_FLAGS.decoupleViewControllerInAnimlib(); + } + + + public static boolean delayShowMagnificationButton() { + return FEATURE_FLAGS.delayShowMagnificationButton(); } - - public static boolean delayedWakelockReleaseOnBackgroundThread() { - return FEATURE_FLAGS.delayedWakelockReleaseOnBackgroundThread(); + + + + public static boolean desktopEffectsQsTile() { + + return FEATURE_FLAGS.desktopEffectsQsTile(); } - + + + public static boolean deviceEntryUdfpsRefactor() { + return FEATURE_FLAGS.deviceEntryUdfpsRefactor(); } - + + + + public static boolean disableBlurredShadeVisible() { + + return FEATURE_FLAGS.disableBlurredShadeVisible(); + } + + + public static boolean disableContextualTipsFrequencyCheck() { + return FEATURE_FLAGS.disableContextualTipsFrequencyCheck(); } - + + + public static boolean disableContextualTipsIosSwitcherCheck() { + return FEATURE_FLAGS.disableContextualTipsIosSwitcherCheck(); } - - public static boolean dozeuiSchedulingAlarmsBackgroundExecution() { - return FEATURE_FLAGS.dozeuiSchedulingAlarmsBackgroundExecution(); + + + + public static boolean disableShadeTrackpadTwoFingerSwipe() { + + return FEATURE_FLAGS.disableShadeTrackpadTwoFingerSwipe(); } - + + + + public static boolean doubleTapToSleep() { + + return FEATURE_FLAGS.doubleTapToSleep(); + } + + + public static boolean dreamInputSessionPilferOnce() { + return FEATURE_FLAGS.dreamInputSessionPilferOnce(); } - + + + public static boolean dreamOverlayBouncerSwipeDirectionFiltering() { + return FEATURE_FLAGS.dreamOverlayBouncerSwipeDirectionFiltering(); } - - public static boolean dualShade() { - return FEATURE_FLAGS.dualShade(); + + + + public static boolean dreamOverlayUpdatedFont() { + + return FEATURE_FLAGS.dreamOverlayUpdatedFont(); } - + + + public static boolean edgeBackGestureHandlerThread() { + return FEATURE_FLAGS.edgeBackGestureHandlerThread(); } - + + + public static boolean edgebackGestureHandlerGetRunningTasksBackground() { + return FEATURE_FLAGS.edgebackGestureHandlerGetRunningTasksBackground(); } - + + + public static boolean enableBackgroundKeyguardOndrawnCallback() { + return FEATURE_FLAGS.enableBackgroundKeyguardOndrawnCallback(); } - + + + public static boolean enableContextualTipForMuteVolume() { + return FEATURE_FLAGS.enableContextualTipForMuteVolume(); } - + + + public static boolean enableContextualTipForPowerOff() { + return FEATURE_FLAGS.enableContextualTipForPowerOff(); } - + + + public static boolean enableContextualTipForTakeScreenshot() { + return FEATURE_FLAGS.enableContextualTipForTakeScreenshot(); } - + + + public static boolean enableContextualTips() { + return FEATURE_FLAGS.enableContextualTips(); } - + + + public static boolean enableEfficientDisplayRepository() { + return FEATURE_FLAGS.enableEfficientDisplayRepository(); } - + + + public static boolean enableLayoutTracing() { + return FEATURE_FLAGS.enableLayoutTracing(); } - + + + + public static boolean enableUnderlay() { + + return FEATURE_FLAGS.enableUnderlay(); + } + + + public static boolean enableViewCaptureTracing() { + return FEATURE_FLAGS.enableViewCaptureTracing(); } - - public static boolean enableWidgetPickerSizeFilter() { - return FEATURE_FLAGS.enableWidgetPickerSizeFilter(); - } - + + + public static boolean enforceBrightnessBaseUserRestriction() { + return FEATURE_FLAGS.enforceBrightnessBaseUserRestriction(); } - + + + public static boolean exampleFlag() { + return FEATURE_FLAGS.exampleFlag(); } - - public static boolean fastUnlockTransition() { - return FEATURE_FLAGS.fastUnlockTransition(); + + + + public static boolean expandCollapsePrivacyDialog() { + + return FEATURE_FLAGS.expandCollapsePrivacyDialog(); } - + + + + public static boolean expandHeadsUpOnInlineReply() { + + return FEATURE_FLAGS.expandHeadsUpOnInlineReply(); + } + + + + public static boolean expandedPrivacyIndicatorsOnLargeScreen() { + + return FEATURE_FLAGS.expandedPrivacyIndicatorsOnLargeScreen(); + } + + + + public static boolean extendedAppsShortcutCategory() { + + return FEATURE_FLAGS.extendedAppsShortcutCategory(); + } + + + + public static boolean faceMessageDeferUpdate() { + + return FEATURE_FLAGS.faceMessageDeferUpdate(); + } + + + + public static boolean faceScanningAnimationNpeFix() { + + return FEATURE_FLAGS.faceScanningAnimationNpeFix(); + } + + + + public static boolean fasterUnlockTransition() { + + return FEATURE_FLAGS.fasterUnlockTransition(); + } + + + + public static boolean fetchBookmarksXmlKeyboardShortcuts() { + + return FEATURE_FLAGS.fetchBookmarksXmlKeyboardShortcuts(); + } + + + public static boolean fixImageWallpaperCrashSurfaceAlreadyReleased() { + return FEATURE_FLAGS.fixImageWallpaperCrashSurfaceAlreadyReleased(); } - + + + public static boolean fixScreenshotActionDismissSystemWindows() { + return FEATURE_FLAGS.fixScreenshotActionDismissSystemWindows(); } - + + + public static boolean floatingMenuAnimatedTuck() { + return FEATURE_FLAGS.floatingMenuAnimatedTuck(); } - + + + + public static boolean floatingMenuDisplayCutoutSupport() { + + return FEATURE_FLAGS.floatingMenuDisplayCutoutSupport(); + } + + + public static boolean floatingMenuDragToEdit() { + return FEATURE_FLAGS.floatingMenuDragToEdit(); } - + + + public static boolean floatingMenuDragToHide() { + return FEATURE_FLAGS.floatingMenuDragToHide(); } - + + + + public static boolean floatingMenuHearingDeviceStatusIcon() { + + return FEATURE_FLAGS.floatingMenuHearingDeviceStatusIcon(); + } + + + public static boolean floatingMenuImeDisplacementAnimation() { + return FEATURE_FLAGS.floatingMenuImeDisplacementAnimation(); } - + + + public static boolean floatingMenuNarrowTargetContentObserver() { + return FEATURE_FLAGS.floatingMenuNarrowTargetContentObserver(); } - + + + + public static boolean floatingMenuNotifyTargetsChangedOnStrictDiff() { + + return FEATURE_FLAGS.floatingMenuNotifyTargetsChangedOnStrictDiff(); + } + + + public static boolean floatingMenuOverlapsNavBarsFlag() { + return FEATURE_FLAGS.floatingMenuOverlapsNavBarsFlag(); } - + + + public static boolean floatingMenuRadiiAnimation() { + return FEATURE_FLAGS.floatingMenuRadiiAnimation(); } - public static boolean generatedPreviews() { - return FEATURE_FLAGS.generatedPreviews(); - } - + + public static boolean getConnectedDeviceNameUnsynchronized() { + return FEATURE_FLAGS.getConnectedDeviceNameUnsynchronized(); } - + + + public static boolean glanceableHubAllowKeyguardWhenDreaming() { + return FEATURE_FLAGS.glanceableHubAllowKeyguardWhenDreaming(); } - - public static boolean glanceableHubFullscreenSwipe() { - return FEATURE_FLAGS.glanceableHubFullscreenSwipe(); + + + + public static boolean glanceableHubBlurredBackground() { + + return FEATURE_FLAGS.glanceableHubBlurredBackground(); } - - public static boolean glanceableHubGestureHandle() { - return FEATURE_FLAGS.glanceableHubGestureHandle(); + + + + public static boolean glanceableHubDirectEditMode() { + + return FEATURE_FLAGS.glanceableHubDirectEditMode(); } - - public static boolean glanceableHubShortcutButton() { - return FEATURE_FLAGS.glanceableHubShortcutButton(); + + + + public static boolean glanceableHubV2() { + + return FEATURE_FLAGS.glanceableHubV2(); } - - public static boolean hapticBrightnessSlider() { - return FEATURE_FLAGS.hapticBrightnessSlider(); + + + + public static boolean glanceableHubV2Resources() { + + return FEATURE_FLAGS.glanceableHubV2Resources(); } - - public static boolean hapticVolumeSlider() { - return FEATURE_FLAGS.hapticVolumeSlider(); + + + + public static boolean hapticsForComposeSliders() { + + return FEATURE_FLAGS.hapticsForComposeSliders(); } - + + + + public static boolean hardwareColorStyles() { + + return FEATURE_FLAGS.hardwareColorStyles(); + } + + + public static boolean hearingAidsQsTileDialog() { + return FEATURE_FLAGS.hearingAidsQsTileDialog(); } - + + + public static boolean hearingDevicesDialogRelatedTools() { + return FEATURE_FLAGS.hearingDevicesDialogRelatedTools(); } - + + + + public static boolean hideRingerButtonInSingleVolumeMode() { + + return FEATURE_FLAGS.hideRingerButtonInSingleVolumeMode(); + } + + + + public static boolean homeControlsDreamHsum() { + + return FEATURE_FLAGS.homeControlsDreamHsum(); + } + + + + public static boolean hubEditModeTouchAdjustments() { + + return FEATURE_FLAGS.hubEditModeTouchAdjustments(); + } + + + + public static boolean hubmodeFullscreenVerticalSwipe() { + + return FEATURE_FLAGS.hubmodeFullscreenVerticalSwipe(); + } + + + + public static boolean hubmodeFullscreenVerticalSwipeFix() { + + return FEATURE_FLAGS.hubmodeFullscreenVerticalSwipeFix(); + } + + + + public static boolean iconRefresh2025() { + + return FEATURE_FLAGS.iconRefresh2025(); + } + + + + public static boolean ignoreTouchesNextToNotificationShelf() { + + return FEATURE_FLAGS.ignoreTouchesNextToNotificationShelf(); + } + + + + public static boolean indicationTextA11yFix() { + + return FEATURE_FLAGS.indicationTextA11yFix(); + } + + + public static boolean keyboardDockingIndicator() { + return FEATURE_FLAGS.keyboardDockingIndicator(); } - + + + public static boolean keyboardShortcutHelperRewrite() { + return FEATURE_FLAGS.keyboardShortcutHelperRewrite(); } - - public static boolean keyguardBottomAreaRefactor() { - return FEATURE_FLAGS.keyguardBottomAreaRefactor(); + + + + public static boolean keyboardShortcutHelperShortcutCustomizer() { + + return FEATURE_FLAGS.keyboardShortcutHelperShortcutCustomizer(); } - + + + + public static boolean keyboardTouchpadContextualEducation() { + + return FEATURE_FLAGS.keyboardTouchpadContextualEducation(); + } + + + + public static boolean keyguardTransitionForceFinishOnScreenOff() { + + return FEATURE_FLAGS.keyguardTransitionForceFinishOnScreenOff(); + } + + + + public static boolean keyguardWmReorderAtmsCalls() { + + return FEATURE_FLAGS.keyguardWmReorderAtmsCalls(); + } + + + public static boolean keyguardWmStateRefactor() { + return FEATURE_FLAGS.keyguardWmStateRefactor(); } - - public static boolean lightRevealMigration() { - return FEATURE_FLAGS.lightRevealMigration(); + + + + public static boolean lockscreenFont() { + + return FEATURE_FLAGS.lockscreenFont(); } - + + + + public static boolean lowLightClockDream() { + + return FEATURE_FLAGS.lowLightClockDream(); + } + + + + public static boolean magneticNotificationSwipes() { + + return FEATURE_FLAGS.magneticNotificationSwipes(); + } + + + + public static boolean mediaControlsA11yColors() { + + return FEATURE_FLAGS.mediaControlsA11yColors(); + } + + + + public static boolean mediaControlsButtonMedia3() { + + return FEATURE_FLAGS.mediaControlsButtonMedia3(); + } + + + + public static boolean mediaControlsButtonMedia3Placement() { + + return FEATURE_FLAGS.mediaControlsButtonMedia3Placement(); + } + + + + public static boolean mediaControlsDeviceManagerBackgroundExecution() { + + return FEATURE_FLAGS.mediaControlsDeviceManagerBackgroundExecution(); + } + + + + public static boolean mediaControlsDrawablesReuseBugfix() { + + return FEATURE_FLAGS.mediaControlsDrawablesReuseBugfix(); + } + + + public static boolean mediaControlsLockscreenShadeBugFix() { + return FEATURE_FLAGS.mediaControlsLockscreenShadeBugFix(); } - - public static boolean mediaControlsRefactor() { - return FEATURE_FLAGS.mediaControlsRefactor(); + + + + public static boolean mediaControlsUiUpdate() { + + return FEATURE_FLAGS.mediaControlsUiUpdate(); } - + + + + public static boolean mediaControlsUmoInflationInBackground() { + + return FEATURE_FLAGS.mediaControlsUmoInflationInBackground(); + } + + + public static boolean mediaControlsUserInitiatedDeleteintent() { + return FEATURE_FLAGS.mediaControlsUserInitiatedDeleteintent(); } - - public static boolean migrateClocksToBlueprint() { - return FEATURE_FLAGS.migrateClocksToBlueprint(); + + + + public static boolean mediaLoadMetadataViaMediaDataLoader() { + + return FEATURE_FLAGS.mediaLoadMetadataViaMediaDataLoader(); } - + + + + public static boolean mediaLockscreenLaunchAnimation() { + + return FEATURE_FLAGS.mediaLockscreenLaunchAnimation(); + } + + + + public static boolean mediaProjectionDialogBehindLockscreen() { + + return FEATURE_FLAGS.mediaProjectionDialogBehindLockscreen(); + } + + + + public static boolean mediaProjectionGreyErrorText() { + + return FEATURE_FLAGS.mediaProjectionGreyErrorText(); + } + + + + public static boolean mediaProjectionRequestAttributionFix() { + + return FEATURE_FLAGS.mediaProjectionRequestAttributionFix(); + } + + + + public static boolean modesUiDialogPaging() { + + return FEATURE_FLAGS.modesUiDialogPaging(); + } + + + + public static boolean moveTransitionAnimationLayer() { + + return FEATURE_FLAGS.moveTransitionAnimationLayer(); + } + + + + public static boolean msdlFeedback() { + + return FEATURE_FLAGS.msdlFeedback(); + } + + + + public static boolean multiuserWifiPickerTrackerSupport() { + + return FEATURE_FLAGS.multiuserWifiPickerTrackerSupport(); + } + + + public static boolean newAodTransition() { + return FEATURE_FLAGS.newAodTransition(); } - - public static boolean newTouchpadGesturesTutorial() { - return FEATURE_FLAGS.newTouchpadGesturesTutorial(); - } - + + + public static boolean newVolumePanel() { + return FEATURE_FLAGS.newVolumePanel(); } - + + + + public static boolean nonTouchscreenDevicesBypassFalsing() { + + return FEATURE_FLAGS.nonTouchscreenDevicesBypassFalsing(); + } + + + + public static boolean notesRoleQsTile() { + + return FEATURE_FLAGS.notesRoleQsTile(); + } + + + + public static boolean notificationAddXOnHoverToDismiss() { + + return FEATURE_FLAGS.notificationAddXOnHoverToDismiss(); + } + + + + public static boolean notificationAmbientSuppressionAfterInflation() { + + return FEATURE_FLAGS.notificationAmbientSuppressionAfterInflation(); + } + + + + public static boolean notificationAnimatedActionsTreatment() { + + return FEATURE_FLAGS.notificationAnimatedActionsTreatment(); + } + + + + public static boolean notificationAppearNonlinear() { + + return FEATURE_FLAGS.notificationAppearNonlinear(); + } + + + public static boolean notificationAsyncGroupHeaderInflation() { + return FEATURE_FLAGS.notificationAsyncGroupHeaderInflation(); } - + + + public static boolean notificationAsyncHybridViewInflation() { + return FEATURE_FLAGS.notificationAsyncHybridViewInflation(); } - + + + public static boolean notificationAvalancheSuppression() { + return FEATURE_FLAGS.notificationAvalancheSuppression(); } - + + + public static boolean notificationAvalancheThrottleHun() { + return FEATURE_FLAGS.notificationAvalancheThrottleHun(); } - + + + public static boolean notificationBackgroundTintOptimization() { + return FEATURE_FLAGS.notificationBackgroundTintOptimization(); } - + + + + public static boolean notificationBundleUi() { + + return FEATURE_FLAGS.notificationBundleUi(); + } + + + public static boolean notificationColorUpdateLogger() { + return FEATURE_FLAGS.notificationColorUpdateLogger(); } - + + + public static boolean notificationContentAlphaOptimization() { + return FEATURE_FLAGS.notificationContentAlphaOptimization(); } - + + + public static boolean notificationFooterBackgroundTintOptimization() { + return FEATURE_FLAGS.notificationFooterBackgroundTintOptimization(); } - - public static boolean notificationMediaManagerBackgroundExecution() { - return FEATURE_FLAGS.notificationMediaManagerBackgroundExecution(); - } - - public static boolean notificationMinimalismPrototype() { - return FEATURE_FLAGS.notificationMinimalismPrototype(); - } - + + + public static boolean notificationOverExpansionClippingFix() { + return FEATURE_FLAGS.notificationOverExpansionClippingFix(); } - - public static boolean notificationPulsingFix() { - return FEATURE_FLAGS.notificationPulsingFix(); + + + + public static boolean notificationReentrantDismiss() { + + return FEATURE_FLAGS.notificationReentrantDismiss(); } - + + + + public static boolean notificationRowAccessibilityExpanded() { + + return FEATURE_FLAGS.notificationRowAccessibilityExpanded(); + } + + + public static boolean notificationRowContentBinderRefactor() { + return FEATURE_FLAGS.notificationRowContentBinderRefactor(); } - + + + + public static boolean notificationRowTransparency() { + + return FEATURE_FLAGS.notificationRowTransparency(); + } + + + public static boolean notificationRowUserContext() { + return FEATURE_FLAGS.notificationRowUserContext(); } - + + + + public static boolean notificationShadeBlur() { + + return FEATURE_FLAGS.notificationShadeBlur(); + } + + + + public static boolean notificationShadeUiThread() { + + return FEATURE_FLAGS.notificationShadeUiThread(); + } + + + + public static boolean notificationSkipSilentUpdates() { + + return FEATURE_FLAGS.notificationSkipSilentUpdates(); + } + + + + public static boolean notificationTransparentHeaderFix() { + + return FEATURE_FLAGS.notificationTransparentHeaderFix(); + } + + + public static boolean notificationViewFlipperPausingV2() { + return FEATURE_FLAGS.notificationViewFlipperPausingV2(); } - + + + public static boolean notificationsBackgroundIcons() { + return FEATURE_FLAGS.notificationsBackgroundIcons(); } - - public static boolean notificationsFooterViewRefactor() { - return FEATURE_FLAGS.notificationsFooterViewRefactor(); + + + + public static boolean notificationsFooterVisibilityFix() { + + return FEATURE_FLAGS.notificationsFooterVisibilityFix(); } - - public static boolean notificationsHeadsUpRefactor() { - return FEATURE_FLAGS.notificationsHeadsUpRefactor(); - } - + + + public static boolean notificationsHideOnDisplaySwitch() { + return FEATURE_FLAGS.notificationsHideOnDisplaySwitch(); } - + + + + public static boolean notificationsHunSharedAnimationValues() { + + return FEATURE_FLAGS.notificationsHunSharedAnimationValues(); + } + + + public static boolean notificationsIconContainerRefactor() { + return FEATURE_FLAGS.notificationsIconContainerRefactor(); } - - public static boolean notificationsImprovedHunAnimation() { - return FEATURE_FLAGS.notificationsImprovedHunAnimation(); + + + + public static boolean notificationsLaunchRadius() { + + return FEATURE_FLAGS.notificationsLaunchRadius(); } - + + + public static boolean notificationsLiveDataStoreRefactor() { + return FEATURE_FLAGS.notificationsLiveDataStoreRefactor(); } - + + + + public static boolean notificationsPinnedHunInShade() { + + return FEATURE_FLAGS.notificationsPinnedHunInShade(); + } + + + + public static boolean notificationsRedesignFooterView() { + + return FEATURE_FLAGS.notificationsRedesignFooterView(); + } + + + + public static boolean notificationsRedesignGuts() { + + return FEATURE_FLAGS.notificationsRedesignGuts(); + } + + + + public static boolean notifyPasswordTextViewUserActivityInBackground() { + + return FEATURE_FLAGS.notifyPasswordTextViewUserActivityInBackground(); + } + + + public static boolean notifyPowerManagerUserActivityBackground() { + return FEATURE_FLAGS.notifyPowerManagerUserActivityBackground(); } - + + + + public static boolean onlyShowMediaStreamSliderInSingleVolumeMode() { + + return FEATURE_FLAGS.onlyShowMediaStreamSliderInSingleVolumeMode(); + } + + + + public static boolean outputSwitcherRedesign() { + + return FEATURE_FLAGS.outputSwitcherRedesign(); + } + + + + public static boolean overrideSuppressOverlayCondition() { + + return FEATURE_FLAGS.overrideSuppressOverlayCondition(); + } + + + + public static boolean permissionHelperInlineUiRichOngoing() { + + return FEATURE_FLAGS.permissionHelperInlineUiRichOngoing(); + } + + + + public static boolean permissionHelperUiRichOngoing() { + + return FEATURE_FLAGS.permissionHelperUiRichOngoing(); + } + + + + public static boolean physicalNotificationMovement() { + + return FEATURE_FLAGS.physicalNotificationMovement(); + } + + + public static boolean pinInputFieldStyledFocusState() { + return FEATURE_FLAGS.pinInputFieldStyledFocusState(); } - - public static boolean predictiveBackAnimateBouncer() { - return FEATURE_FLAGS.predictiveBackAnimateBouncer(); - } - - public static boolean predictiveBackAnimateDialogs() { - return FEATURE_FLAGS.predictiveBackAnimateDialogs(); - } - + + + public static boolean predictiveBackAnimateShade() { + return FEATURE_FLAGS.predictiveBackAnimateShade(); } - - public static boolean predictiveBackSysui() { - return FEATURE_FLAGS.predictiveBackSysui(); + + + + public static boolean predictiveBackDelayWmTransition() { + + return FEATURE_FLAGS.predictiveBackDelayWmTransition(); } - + + + public static boolean priorityPeopleSection() { + return FEATURE_FLAGS.priorityPeopleSection(); } - - public static boolean privacyDotUnfoldWrongCornerFix() { - return FEATURE_FLAGS.privacyDotUnfoldWrongCornerFix(); + + + + public static boolean promoteNotificationsAutomatically() { + + return FEATURE_FLAGS.promoteNotificationsAutomatically(); } - - public static boolean pssAppSelectorAbruptExitFix() { - return FEATURE_FLAGS.pssAppSelectorAbruptExitFix(); - } - + + + public static boolean pssAppSelectorRecentsSplitScreen() { + return FEATURE_FLAGS.pssAppSelectorRecentsSplitScreen(); } - + + + public static boolean pssTaskSwitcher() { + return FEATURE_FLAGS.pssTaskSwitcher(); } - + + + public static boolean qsCustomTileClickGuaranteedBugFix() { + return FEATURE_FLAGS.qsCustomTileClickGuaranteedBugFix(); } - - public static boolean qsNewPipeline() { - return FEATURE_FLAGS.qsNewPipeline(); - } - + + + public static boolean qsNewTiles() { + return FEATURE_FLAGS.qsNewTiles(); } - + + + public static boolean qsNewTilesFuture() { + return FEATURE_FLAGS.qsNewTilesFuture(); } - + + + + public static boolean qsQuickRebindActiveTiles() { + + return FEATURE_FLAGS.qsQuickRebindActiveTiles(); + } + + + + public static boolean qsRegisterSettingObserverOnBgThread() { + + return FEATURE_FLAGS.qsRegisterSettingObserverOnBgThread(); + } + + + + public static boolean qsTileDetailedView() { + + return FEATURE_FLAGS.qsTileDetailedView(); + } + + + public static boolean qsTileFocusState() { + return FEATURE_FLAGS.qsTileFocusState(); } - + + + public static boolean qsUiRefactor() { + return FEATURE_FLAGS.qsUiRefactor(); } - - public static boolean quickSettingsVisualHapticsLongpress() { - return FEATURE_FLAGS.quickSettingsVisualHapticsLongpress(); + + + + public static boolean qsUiRefactorComposeFragment() { + + return FEATURE_FLAGS.qsUiRefactorComposeFragment(); } - + + + public static boolean recordIssueQsTile() { + return FEATURE_FLAGS.recordIssueQsTile(); } - + + + + public static boolean redesignMagnificationWindowSize() { + + return FEATURE_FLAGS.redesignMagnificationWindowSize(); + } + + + public static boolean refactorGetCurrentUser() { + return FEATURE_FLAGS.refactorGetCurrentUser(); } - + + + public static boolean registerBatteryControllerReceiversInCorestartable() { + return FEATURE_FLAGS.registerBatteryControllerReceiversInCorestartable(); } - + + + + public static boolean registerContentObserversAsync() { + + return FEATURE_FLAGS.registerContentObserversAsync(); + } + + + public static boolean registerNewWalletCardInBackground() { + return FEATURE_FLAGS.registerNewWalletCardInBackground(); } - + + + public static boolean registerWallpaperNotifierBackground() { + return FEATURE_FLAGS.registerWallpaperNotifierBackground(); } - - public static boolean registerZenModeContentObserverBackground() { - return FEATURE_FLAGS.registerZenModeContentObserverBackground(); + + + + public static boolean relockWithPowerButtonImmediately() { + + return FEATURE_FLAGS.relockWithPowerButtonImmediately(); } - + + + public static boolean removeDreamOverlayHideOnTouch() { + return FEATURE_FLAGS.removeDreamOverlayHideOnTouch(); } - + + + + public static boolean removeUpdateListenerInQsIconViewImpl() { + + return FEATURE_FLAGS.removeUpdateListenerInQsIconViewImpl(); + } + + + public static boolean restToUnlock() { + return FEATURE_FLAGS.restToUnlock(); } - + + + public static boolean restartDreamOnUnocclude() { + return FEATURE_FLAGS.restartDreamOnUnocclude(); } - + + + public static boolean revampedBouncerMessages() { + return FEATURE_FLAGS.revampedBouncerMessages(); } - + + + public static boolean runFingerprintDetectOnDismissibleKeyguard() { + return FEATURE_FLAGS.runFingerprintDetectOnDismissibleKeyguard(); } - + + + public static boolean saveAndRestoreMagnificationSettingsButtons() { + return FEATURE_FLAGS.saveAndRestoreMagnificationSettingsButtons(); } - + + + public static boolean sceneContainer() { + return FEATURE_FLAGS.sceneContainer(); } - + + + public static boolean screenshareNotificationHidingBugFix() { + return FEATURE_FLAGS.screenshareNotificationHidingBugFix(); } - + + + public static boolean screenshotActionDismissSystemWindows() { + return FEATURE_FLAGS.screenshotActionDismissSystemWindows(); } - - public static boolean screenshotPrivateProfileAccessibilityAnnouncementFix() { - return FEATURE_FLAGS.screenshotPrivateProfileAccessibilityAnnouncementFix(); + + + + public static boolean screenshotMultidisplayFocusChange() { + + return FEATURE_FLAGS.screenshotMultidisplayFocusChange(); } - - public static boolean screenshotPrivateProfileBehaviorFix() { - return FEATURE_FLAGS.screenshotPrivateProfileBehaviorFix(); + + + + public static boolean screenshotPolicySplitAndDesktopMode() { + + return FEATURE_FLAGS.screenshotPolicySplitAndDesktopMode(); } - + + + public static boolean screenshotScrollCropViewCrashFix() { + return FEATURE_FLAGS.screenshotScrollCropViewCrashFix(); } - - public static boolean screenshotShelfUi2() { - return FEATURE_FLAGS.screenshotShelfUi2(); + + + + public static boolean screenshotUiControllerRefactor() { + + return FEATURE_FLAGS.screenshotUiControllerRefactor(); } - - public static boolean shadeCollapseActivityLaunchFix() { - return FEATURE_FLAGS.shadeCollapseActivityLaunchFix(); + + + + public static boolean secondaryUserWidgetHost() { + + return FEATURE_FLAGS.secondaryUserWidgetHost(); } - + + + + public static boolean settingsExtRegisterContentObserverOnBgThread() { + + return FEATURE_FLAGS.settingsExtRegisterContentObserverOnBgThread(); + } + + + + public static boolean shadeExpandsOnStatusBarLongPress() { + + return FEATURE_FLAGS.shadeExpandsOnStatusBarLongPress(); + } + + + + public static boolean shadeHeaderFontUpdate() { + + return FEATURE_FLAGS.shadeHeaderFontUpdate(); + } + + + + public static boolean shadeLaunchAccessibility() { + + return FEATURE_FLAGS.shadeLaunchAccessibility(); + } + + + + public static boolean shadeWindowGoesAround() { + + return FEATURE_FLAGS.shadeWindowGoesAround(); + } + + + public static boolean shaderlibLoadingEffectRefactor() { + return FEATURE_FLAGS.shaderlibLoadingEffectRefactor(); } - + + + + public static boolean shortcutHelperKeyGlyph() { + + return FEATURE_FLAGS.shortcutHelperKeyGlyph(); + } + + + + public static boolean showAudioSharingSliderInVolumePanel() { + + return FEATURE_FLAGS.showAudioSharingSliderInVolumePanel(); + } + + + + public static boolean showClipboardIndication() { + + return FEATURE_FLAGS.showClipboardIndication(); + } + + + + public static boolean showLockedByYourWatchKeyguardIndicator() { + + return FEATURE_FLAGS.showLockedByYourWatchKeyguardIndicator(); + } + + + + public static boolean showToastWhenAppControlBrightness() { + + return FEATURE_FLAGS.showToastWhenAppControlBrightness(); + } + + + + public static boolean simPinBouncerReset() { + + return FEATURE_FLAGS.simPinBouncerReset(); + } + + + + public static boolean simPinRaceConditionOnRestart() { + + return FEATURE_FLAGS.simPinRaceConditionOnRestart(); + } + + + + public static boolean simPinUseSlotId() { + + return FEATURE_FLAGS.simPinUseSlotId(); + } + + + + public static boolean skipHideSensitiveNotifAnimation() { + + return FEATURE_FLAGS.skipHideSensitiveNotifAnimation(); + } + + + public static boolean sliceBroadcastRelayInBackground() { + return FEATURE_FLAGS.sliceBroadcastRelayInBackground(); } - + + + public static boolean sliceManagerBinderCallBackground() { + return FEATURE_FLAGS.sliceManagerBinderCallBackground(); } - + + + public static boolean smartspaceLockscreenViewmodel() { + return FEATURE_FLAGS.smartspaceLockscreenViewmodel(); } - + + + public static boolean smartspaceRelocateToBottom() { + return FEATURE_FLAGS.smartspaceRelocateToBottom(); } - - public static boolean smartspaceRemoteviewsRendering() { - return FEATURE_FLAGS.smartspaceRemoteviewsRendering(); + + + + public static boolean smartspaceRemoteviewsRenderingFix() { + + return FEATURE_FLAGS.smartspaceRemoteviewsRenderingFix(); } - + + + + public static boolean smartspaceSwipeEventLoggingFix() { + + return FEATURE_FLAGS.smartspaceSwipeEventLoggingFix(); + } + + + + public static boolean smartspaceViewpager2() { + + return FEATURE_FLAGS.smartspaceViewpager2(); + } + + + + public static boolean sounddoseCustomization() { + + return FEATURE_FLAGS.sounddoseCustomization(); + } + + + + public static boolean spatialModelAppPushback() { + + return FEATURE_FLAGS.spatialModelAppPushback(); + } + + + + public static boolean stabilizeHeadsUpGroupV2() { + + return FEATURE_FLAGS.stabilizeHeadsUpGroupV2(); + } + + + + public static boolean statusBarAlwaysCheckUnderlyingNetworks() { + + return FEATURE_FLAGS.statusBarAlwaysCheckUnderlyingNetworks(); + } + + + + public static boolean statusBarAutoStartScreenRecordChip() { + + return FEATURE_FLAGS.statusBarAutoStartScreenRecordChip(); + } + + + + public static boolean statusBarChipsModernization() { + + return FEATURE_FLAGS.statusBarChipsModernization(); + } + + + + public static boolean statusBarChipsReturnAnimations() { + + return FEATURE_FLAGS.statusBarChipsReturnAnimations(); + } + + + + public static boolean statusBarFontUpdates() { + + return FEATURE_FLAGS.statusBarFontUpdates(); + } + + + + public static boolean statusBarMobileIconKairos() { + + return FEATURE_FLAGS.statusBarMobileIconKairos(); + } + + + public static boolean statusBarMonochromeIconsFix() { + return FEATURE_FLAGS.statusBarMonochromeIconsFix(); } - - public static boolean statusBarScreenSharingChips() { - return FEATURE_FLAGS.statusBarScreenSharingChips(); + + + + public static boolean statusBarNoHunBehavior() { + + return FEATURE_FLAGS.statusBarNoHunBehavior(); } - + + + + public static boolean statusBarPopupChips() { + + return FEATURE_FLAGS.statusBarPopupChips(); + } + + + + public static boolean statusBarRootModernization() { + + return FEATURE_FLAGS.statusBarRootModernization(); + } + + + + public static boolean statusBarShowAudioOnlyProjectionChip() { + + return FEATURE_FLAGS.statusBarShowAudioOnlyProjectionChip(); + } + + + + public static boolean statusBarSignalPolicyRefactor() { + + return FEATURE_FLAGS.statusBarSignalPolicyRefactor(); + } + + + + public static boolean statusBarSignalPolicyRefactorEthernet() { + + return FEATURE_FLAGS.statusBarSignalPolicyRefactorEthernet(); + } + + + public static boolean statusBarStaticInoutIndicators() { + return FEATURE_FLAGS.statusBarStaticInoutIndicators(); } - + + + + public static boolean statusBarStopUpdatingWindowHeight() { + + return FEATURE_FLAGS.statusBarStopUpdatingWindowHeight(); + } + + + + public static boolean statusBarSwipeOverChip() { + + return FEATURE_FLAGS.statusBarSwipeOverChip(); + } + + + + public static boolean statusBarSwitchToSpnFromDataSpn() { + + return FEATURE_FLAGS.statusBarSwitchToSpnFromDataSpn(); + } + + + + public static boolean statusBarUiThread() { + + return FEATURE_FLAGS.statusBarUiThread(); + } + + + + public static boolean statusBarWindowNoCustomTouch() { + + return FEATURE_FLAGS.statusBarWindowNoCustomTouch(); + } + + + + public static boolean stoppableFgsSystemApp() { + + return FEATURE_FLAGS.stoppableFgsSystemApp(); + } + + + public static boolean switchUserOnBg() { + return FEATURE_FLAGS.switchUserOnBg(); } - + + + public static boolean sysuiTeamfood() { + return FEATURE_FLAGS.sysuiTeamfood(); } - + + + public static boolean themeOverlayControllerWakefulnessDeprecation() { + return FEATURE_FLAGS.themeOverlayControllerWakefulnessDeprecation(); } - + + + + public static boolean transitionRaceCondition() { + + return FEATURE_FLAGS.transitionRaceCondition(); + } + + + public static boolean translucentOccludingActivityFix() { + return FEATURE_FLAGS.translucentOccludingActivityFix(); } - - public static boolean truncatedStatusBarIconsFix() { - return FEATURE_FLAGS.truncatedStatusBarIconsFix(); + + + + public static boolean tvGlobalActionsFocus() { + + return FEATURE_FLAGS.tvGlobalActionsFocus(); } - + + + public static boolean udfpsViewPerformance() { + return FEATURE_FLAGS.udfpsViewPerformance(); } - + + + public static boolean unfoldAnimationBackgroundProgress() { + return FEATURE_FLAGS.unfoldAnimationBackgroundProgress(); } - + + + + public static boolean unfoldLatencyTrackingFix() { + + return FEATURE_FLAGS.unfoldLatencyTrackingFix(); + } + + + + public static boolean updateCornerRadiusOnDisplayChanged() { + + return FEATURE_FLAGS.updateCornerRadiusOnDisplayChanged(); + } + + + public static boolean updateUserSwitcherBackground() { + return FEATURE_FLAGS.updateUserSwitcherBackground(); } - - public static boolean validateKeyboardShortcutHelperIconUri() { - return FEATURE_FLAGS.validateKeyboardShortcutHelperIconUri(); + + + + public static boolean updateWindowMagnifierBottomBoundary() { + + return FEATURE_FLAGS.updateWindowMagnifierBottomBoundary(); } - + + + + public static boolean useAadProxSensor() { + + return FEATURE_FLAGS.useAadProxSensor(); + } + + + + public static boolean useNotifInflationThreadForFooter() { + + return FEATURE_FLAGS.useNotifInflationThreadForFooter(); + } + + + + public static boolean useNotifInflationThreadForRow() { + + return FEATURE_FLAGS.useNotifInflationThreadForRow(); + } + + + + public static boolean useTransitionsForKeyguardOccluded() { + + return FEATURE_FLAGS.useTransitionsForKeyguardOccluded(); + } + + + + public static boolean useVolumeController() { + + return FEATURE_FLAGS.useVolumeController(); + } + + + + public static boolean userAwareSettingsRepositories() { + + return FEATURE_FLAGS.userAwareSettingsRepositories(); + } + + + + public static boolean userEncryptedSource() { + + return FEATURE_FLAGS.userEncryptedSource(); + } + + + + public static boolean userSwitcherAddSignOutOption() { + + return FEATURE_FLAGS.userSwitcherAddSignOutOption(); + } + + + public static boolean visualInterruptionsRefactor() { + return FEATURE_FLAGS.visualInterruptionsRefactor(); } + + + public static boolean volumeRedesign() { + + return FEATURE_FLAGS.volumeRedesign(); + } + private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); } diff --git a/flags/src/com/android/systemui/shared/CustomFeatureFlags.java b/flags/src/com/android/systemui/shared/CustomFeatureFlags.java index b301340b4a..18f1f2b21d 100644 --- a/flags/src/com/android/systemui/shared/CustomFeatureFlags.java +++ b/flags/src/com/android/systemui/shared/CustomFeatureFlags.java @@ -1,13 +1,13 @@ package com.android.systemui.shared; // TODO(b/303773055): Remove the annotation after access issue is resolved. + import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Predicate; - /** @hide */ public class CustomFeatureFlags implements FeatureFlags { @@ -17,56 +17,182 @@ public class CustomFeatureFlags implements FeatureFlags { mGetValueImpl = getValueImpl; } @Override - + + public boolean ambientAod() { + return getValue(Flags.FLAG_AMBIENT_AOD, + FeatureFlags::ambientAod); + } + + @Override + public boolean bouncerAreaExclusion() { return getValue(Flags.FLAG_BOUNCER_AREA_EXCLUSION, - FeatureFlags::bouncerAreaExclusion); + FeatureFlags::bouncerAreaExclusion); } @Override - + + public boolean clockReactiveSmartspaceLayout() { + return getValue(Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT, + FeatureFlags::clockReactiveSmartspaceLayout); + } + + @Override + + public boolean clockReactiveVariants() { + return getValue(Flags.FLAG_CLOCK_REACTIVE_VARIANTS, + FeatureFlags::clockReactiveVariants); + } + + @Override + + public boolean cursorHotCorner() { + return getValue(Flags.FLAG_CURSOR_HOT_CORNER, + FeatureFlags::cursorHotCorner); + } + + @Override + public boolean enableHomeDelay() { return getValue(Flags.FLAG_ENABLE_HOME_DELAY, - FeatureFlags::enableHomeDelay); + FeatureFlags::enableHomeDelay); } @Override - + + public boolean enableLppSqueezeEffect() { + return getValue(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT, + FeatureFlags::enableLppSqueezeEffect); + } + + @Override + public boolean exampleSharedFlag() { return getValue(Flags.FLAG_EXAMPLE_SHARED_FLAG, - FeatureFlags::exampleSharedFlag); + FeatureFlags::exampleSharedFlag); } @Override - + + public boolean extendedWallpaperEffects() { + return getValue(Flags.FLAG_EXTENDED_WALLPAPER_EFFECTS, + FeatureFlags::extendedWallpaperEffects); + } + + @Override + + public boolean lockscreenCustomClocks() { + return getValue(Flags.FLAG_LOCKSCREEN_CUSTOM_CLOCKS, + FeatureFlags::lockscreenCustomClocks); + } + + @Override + + public boolean newCustomizationPickerUi() { + return getValue(Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI, + FeatureFlags::newCustomizationPickerUi); + } + + @Override + + public boolean newTouchpadGesturesTutorial() { + return getValue(Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, + FeatureFlags::newTouchpadGesturesTutorial); + } + + @Override + public boolean returnAnimationFrameworkLibrary() { return getValue(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - FeatureFlags::returnAnimationFrameworkLibrary); + FeatureFlags::returnAnimationFrameworkLibrary); } @Override - + + public boolean returnAnimationFrameworkLongLived() { + return getValue(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + FeatureFlags::returnAnimationFrameworkLongLived); + } + + @Override + + public boolean screenshotContextUrl() { + return getValue(Flags.FLAG_SCREENSHOT_CONTEXT_URL, + FeatureFlags::screenshotContextUrl); + } + + @Override + public boolean shadeAllowBackGesture() { return getValue(Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, - FeatureFlags::shadeAllowBackGesture); + FeatureFlags::shadeAllowBackGesture); } @Override - + public boolean sidefpsControllerRefactor() { return getValue(Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR, - FeatureFlags::sidefpsControllerRefactor); + FeatureFlags::sidefpsControllerRefactor); + } + + @Override + + public boolean smartspaceRemoteviewsIntentHandler() { + return getValue(Flags.FLAG_SMARTSPACE_REMOTEVIEWS_INTENT_HANDLER, + FeatureFlags::smartspaceRemoteviewsIntentHandler); + } + + @Override + + public boolean smartspaceSportsCardBackground() { + return getValue(Flags.FLAG_SMARTSPACE_SPORTS_CARD_BACKGROUND, + FeatureFlags::smartspaceSportsCardBackground); + } + + @Override + + public boolean smartspaceUiUpdate() { + return getValue(Flags.FLAG_SMARTSPACE_UI_UPDATE, + FeatureFlags::smartspaceUiUpdate); + } + + @Override + + public boolean smartspaceUiUpdateResources() { + return getValue(Flags.FLAG_SMARTSPACE_UI_UPDATE_RESOURCES, + FeatureFlags::smartspaceUiUpdateResources); + } + + @Override + + public boolean statusBarConnectedDisplays() { + return getValue(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS, + FeatureFlags::statusBarConnectedDisplays); + } + + @Override + + public boolean threeButtonCornerSwipe() { + return getValue(Flags.FLAG_THREE_BUTTON_CORNER_SWIPE, + FeatureFlags::threeButtonCornerSwipe); + } + + @Override + + public boolean usePreferredImageEditor() { + return getValue(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR, + FeatureFlags::usePreferredImageEditor); } public boolean isFlagReadOnlyOptimized(String flagName) { if (mReadOnlyFlagsSet.contains(flagName) && - isOptimizationEnabled()) { - return true; + isOptimizationEnabled()) { + return true; } return false; } - + private boolean isOptimizationEnabled() { return false; } @@ -77,18 +203,60 @@ public class CustomFeatureFlags implements FeatureFlags { public List getFlagNames() { return Arrays.asList( - Flags.FLAG_BOUNCER_AREA_EXCLUSION, - Flags.FLAG_ENABLE_HOME_DELAY, - Flags.FLAG_EXAMPLE_SHARED_FLAG, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, - Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR + Flags.FLAG_AMBIENT_AOD, + Flags.FLAG_BOUNCER_AREA_EXCLUSION, + Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT, + Flags.FLAG_CLOCK_REACTIVE_VARIANTS, + Flags.FLAG_CURSOR_HOT_CORNER, + Flags.FLAG_ENABLE_HOME_DELAY, + Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT, + Flags.FLAG_EXAMPLE_SHARED_FLAG, + Flags.FLAG_EXTENDED_WALLPAPER_EFFECTS, + Flags.FLAG_LOCKSCREEN_CUSTOM_CLOCKS, + Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI, + Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + Flags.FLAG_SCREENSHOT_CONTEXT_URL, + Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, + Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR, + Flags.FLAG_SMARTSPACE_REMOTEVIEWS_INTENT_HANDLER, + Flags.FLAG_SMARTSPACE_SPORTS_CARD_BACKGROUND, + Flags.FLAG_SMARTSPACE_UI_UPDATE, + Flags.FLAG_SMARTSPACE_UI_UPDATE_RESOURCES, + Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS, + Flags.FLAG_THREE_BUTTON_CORNER_SWIPE, + Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR ); } private Set mReadOnlyFlagsSet = new HashSet<>( - Arrays.asList( - "" - ) + Arrays.asList( + Flags.FLAG_AMBIENT_AOD, + Flags.FLAG_BOUNCER_AREA_EXCLUSION, + Flags.FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT, + Flags.FLAG_CLOCK_REACTIVE_VARIANTS, + Flags.FLAG_CURSOR_HOT_CORNER, + Flags.FLAG_ENABLE_HOME_DELAY, + Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT, + Flags.FLAG_EXAMPLE_SHARED_FLAG, + Flags.FLAG_EXTENDED_WALLPAPER_EFFECTS, + Flags.FLAG_LOCKSCREEN_CUSTOM_CLOCKS, + Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI, + Flags.FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + Flags.FLAG_SCREENSHOT_CONTEXT_URL, + Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, + Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR, + Flags.FLAG_SMARTSPACE_REMOTEVIEWS_INTENT_HANDLER, + Flags.FLAG_SMARTSPACE_SPORTS_CARD_BACKGROUND, + Flags.FLAG_SMARTSPACE_UI_UPDATE, + Flags.FLAG_SMARTSPACE_UI_UPDATE_RESOURCES, + Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS, + Flags.FLAG_THREE_BUTTON_CORNER_SWIPE, + Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR, + "" + ) ); } diff --git a/flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java b/flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java index c223248bbf..226db51996 100644 --- a/flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java +++ b/flags/src/com/android/systemui/shared/FakeFeatureFlagsImpl.java @@ -3,7 +3,6 @@ package com.android.systemui.shared; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; - /** @hide */ public class FakeFeatureFlagsImpl extends CustomFeatureFlags { private final Map mFlagMap = new HashMap<>(); diff --git a/flags/src/com/android/systemui/shared/FeatureFlags.java b/flags/src/com/android/systemui/shared/FeatureFlags.java index e9fe9c7144..7f14ce4149 100644 --- a/flags/src/com/android/systemui/shared/FeatureFlags.java +++ b/flags/src/com/android/systemui/shared/FeatureFlags.java @@ -1,24 +1,103 @@ package com.android.systemui.shared; // TODO(b/303773055): Remove the annotation after access issue is resolved. + /** @hide */ public interface FeatureFlags { - - + + + + boolean ambientAod(); + + + boolean bouncerAreaExclusion(); - - + + + + boolean clockReactiveSmartspaceLayout(); + + + + boolean clockReactiveVariants(); + + + + boolean cursorHotCorner(); + + + boolean enableHomeDelay(); - - + + + + boolean enableLppSqueezeEffect(); + + + boolean exampleSharedFlag(); - - + + + + boolean extendedWallpaperEffects(); + + + + boolean lockscreenCustomClocks(); + + + + boolean newCustomizationPickerUi(); + + + + boolean newTouchpadGesturesTutorial(); + + + boolean returnAnimationFrameworkLibrary(); - - + + + + boolean returnAnimationFrameworkLongLived(); + + + + boolean screenshotContextUrl(); + + + boolean shadeAllowBackGesture(); - - + + + boolean sidefpsControllerRefactor(); + + + + boolean smartspaceRemoteviewsIntentHandler(); + + + + boolean smartspaceSportsCardBackground(); + + + + boolean smartspaceUiUpdate(); + + + + boolean smartspaceUiUpdateResources(); + + + + boolean statusBarConnectedDisplays(); + + + + boolean threeButtonCornerSwipe(); + + + + boolean usePreferredImageEditor(); } diff --git a/flags/src/com/android/systemui/shared/FeatureFlagsImpl.java b/flags/src/com/android/systemui/shared/FeatureFlagsImpl.java index 39a1f0ee3a..27cc82cf05 100644 --- a/flags/src/com/android/systemui/shared/FeatureFlagsImpl.java +++ b/flags/src/com/android/systemui/shared/FeatureFlagsImpl.java @@ -1,195 +1,174 @@ package com.android.systemui.shared; // TODO(b/303773055): Remove the annotation after access issue is resolved. -import com.android.quickstep.util.DeviceConfigHelper; - -import java.nio.file.Files; -import java.nio.file.Paths; /** @hide */ public final class FeatureFlagsImpl implements FeatureFlags { - private static final boolean isReadFromNew = Files.exists(Paths.get("/metadata/aconfig/boot/enable_only_new_storage")); - private static volatile boolean isCached = false; - private static volatile boolean biometrics_framework_is_cached = false; - private static volatile boolean systemui_is_cached = false; - private static boolean bouncerAreaExclusion = true; - private static boolean enableHomeDelay = false; - private static boolean exampleSharedFlag = false; - private static boolean returnAnimationFrameworkLibrary = false; - private static boolean shadeAllowBackGesture = false; - private static boolean sidefpsControllerRefactor = true; + @Override - private void init() { - boolean foundPackage = true; - - sidefpsControllerRefactor = foundPackage; - - - bouncerAreaExclusion = foundPackage; - - - enableHomeDelay = foundPackage; - - - exampleSharedFlag = foundPackage; - - - returnAnimationFrameworkLibrary = foundPackage ; - - - shadeAllowBackGesture = foundPackage; - - isCached = true; - } - - - - - private void load_overrides_biometrics_framework() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - sidefpsControllerRefactor = - properties.getBoolean(Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace biometrics_framework " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - biometrics_framework_is_cached = true; - } - - private void load_overrides_systemui() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - bouncerAreaExclusion = - properties.getBoolean(Flags.FLAG_BOUNCER_AREA_EXCLUSION, true); - enableHomeDelay = - properties.getBoolean(Flags.FLAG_ENABLE_HOME_DELAY, false); - exampleSharedFlag = - properties.getBoolean(Flags.FLAG_EXAMPLE_SHARED_FLAG, false); - returnAnimationFrameworkLibrary = - properties.getBoolean(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, false); - shadeAllowBackGesture = - properties.getBoolean(Flags.FLAG_SHADE_ALLOW_BACK_GESTURE, false); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace systemui " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - systemui_is_cached = true; + public boolean ambientAod() { + return false; } @Override - - + + public boolean bouncerAreaExclusion() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return bouncerAreaExclusion; - + return true; } @Override - - + + + public boolean clockReactiveSmartspaceLayout() { + return false; + } + + @Override + + + public boolean clockReactiveVariants() { + return false; + } + + @Override + + + public boolean cursorHotCorner() { + return false; + } + + @Override + + public boolean enableHomeDelay() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return enableHomeDelay; - + return false; } @Override - - + + + public boolean enableLppSqueezeEffect() { + return false; + } + + @Override + + public boolean exampleSharedFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return exampleSharedFlag; - + return false; } @Override - - + + + public boolean extendedWallpaperEffects() { + return false; + } + + @Override + + + public boolean lockscreenCustomClocks() { + return false; + } + + @Override + + + public boolean newCustomizationPickerUi() { + return false; + } + + @Override + + + public boolean newTouchpadGesturesTutorial() { + return true; + } + + @Override + + public boolean returnAnimationFrameworkLibrary() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return returnAnimationFrameworkLibrary; - + return true; } @Override - - + + + public boolean returnAnimationFrameworkLongLived() { + return true; + } + + @Override + + + public boolean screenshotContextUrl() { + return false; + } + + @Override + + public boolean shadeAllowBackGesture() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return shadeAllowBackGesture; - + return false; } @Override - - - public boolean sidefpsControllerRefactor() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!biometrics_framework_is_cached) { - load_overrides_biometrics_framework(); - } - } - return sidefpsControllerRefactor; + + public boolean sidefpsControllerRefactor() { + return true; + } + + @Override + + + public boolean smartspaceRemoteviewsIntentHandler() { + return true; + } + + @Override + + + public boolean smartspaceSportsCardBackground() { + return false; + } + + @Override + + + public boolean smartspaceUiUpdate() { + return false; + } + + @Override + + + public boolean smartspaceUiUpdateResources() { + return false; + } + + @Override + + + public boolean statusBarConnectedDisplays() { + return false; + } + + @Override + + + public boolean threeButtonCornerSwipe() { + return false; + } + + @Override + + + public boolean usePreferredImageEditor() { + return false; } } - diff --git a/flags/src/com/android/systemui/shared/Flags.java b/flags/src/com/android/systemui/shared/Flags.java index 4b817be4ef..26a3ca3f47 100644 --- a/flags/src/com/android/systemui/shared/Flags.java +++ b/flags/src/com/android/systemui/shared/Flags.java @@ -1,50 +1,226 @@ package com.android.systemui.shared; // TODO(b/303773055): Remove the annotation after access issue is resolved. + + /** @hide */ public final class Flags { + /** @hide */ + public static final String FLAG_AMBIENT_AOD = "com.android.systemui.shared.ambient_aod"; /** @hide */ public static final String FLAG_BOUNCER_AREA_EXCLUSION = "com.android.systemui.shared.bouncer_area_exclusion"; /** @hide */ + public static final String FLAG_CLOCK_REACTIVE_SMARTSPACE_LAYOUT = "com.android.systemui.shared.clock_reactive_smartspace_layout"; + /** @hide */ + public static final String FLAG_CLOCK_REACTIVE_VARIANTS = "com.android.systemui.shared.clock_reactive_variants"; + /** @hide */ + public static final String FLAG_CURSOR_HOT_CORNER = "com.android.systemui.shared.cursor_hot_corner"; + /** @hide */ public static final String FLAG_ENABLE_HOME_DELAY = "com.android.systemui.shared.enable_home_delay"; /** @hide */ + public static final String FLAG_ENABLE_LPP_SQUEEZE_EFFECT = "com.android.systemui.shared.enable_lpp_squeeze_effect"; + /** @hide */ public static final String FLAG_EXAMPLE_SHARED_FLAG = "com.android.systemui.shared.example_shared_flag"; /** @hide */ + public static final String FLAG_EXTENDED_WALLPAPER_EFFECTS = "com.android.systemui.shared.extended_wallpaper_effects"; + /** @hide */ + public static final String FLAG_LOCKSCREEN_CUSTOM_CLOCKS = "com.android.systemui.shared.lockscreen_custom_clocks"; + /** @hide */ + public static final String FLAG_NEW_CUSTOMIZATION_PICKER_UI = "com.android.systemui.shared.new_customization_picker_ui"; + /** @hide */ + public static final String FLAG_NEW_TOUCHPAD_GESTURES_TUTORIAL = "com.android.systemui.shared.new_touchpad_gestures_tutorial"; + /** @hide */ public static final String FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY = "com.android.systemui.shared.return_animation_framework_library"; /** @hide */ + public static final String FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED = "com.android.systemui.shared.return_animation_framework_long_lived"; + /** @hide */ + public static final String FLAG_SCREENSHOT_CONTEXT_URL = "com.android.systemui.shared.screenshot_context_url"; + /** @hide */ public static final String FLAG_SHADE_ALLOW_BACK_GESTURE = "com.android.systemui.shared.shade_allow_back_gesture"; /** @hide */ public static final String FLAG_SIDEFPS_CONTROLLER_REFACTOR = "com.android.systemui.shared.sidefps_controller_refactor"; - - + /** @hide */ + public static final String FLAG_SMARTSPACE_REMOTEVIEWS_INTENT_HANDLER = "com.android.systemui.shared.smartspace_remoteviews_intent_handler"; + /** @hide */ + public static final String FLAG_SMARTSPACE_SPORTS_CARD_BACKGROUND = "com.android.systemui.shared.smartspace_sports_card_background"; + /** @hide */ + public static final String FLAG_SMARTSPACE_UI_UPDATE = "com.android.systemui.shared.smartspace_ui_update"; + /** @hide */ + public static final String FLAG_SMARTSPACE_UI_UPDATE_RESOURCES = "com.android.systemui.shared.smartspace_ui_update_resources"; + /** @hide */ + public static final String FLAG_STATUS_BAR_CONNECTED_DISPLAYS = "com.android.systemui.shared.status_bar_connected_displays"; + /** @hide */ + public static final String FLAG_THREE_BUTTON_CORNER_SWIPE = "com.android.systemui.shared.three_button_corner_swipe"; + /** @hide */ + public static final String FLAG_USE_PREFERRED_IMAGE_EDITOR = "com.android.systemui.shared.use_preferred_image_editor"; + + + + public static boolean ambientAod() { + + return FEATURE_FLAGS.ambientAod(); + } + + + public static boolean bouncerAreaExclusion() { + return FEATURE_FLAGS.bouncerAreaExclusion(); } - - + + + + public static boolean clockReactiveSmartspaceLayout() { + + return FEATURE_FLAGS.clockReactiveSmartspaceLayout(); + } + + + + public static boolean clockReactiveVariants() { + + return FEATURE_FLAGS.clockReactiveVariants(); + } + + + + public static boolean cursorHotCorner() { + + return FEATURE_FLAGS.cursorHotCorner(); + } + + + public static boolean enableHomeDelay() { + return FEATURE_FLAGS.enableHomeDelay(); } - - + + + + public static boolean enableLppSqueezeEffect() { + + return FEATURE_FLAGS.enableLppSqueezeEffect(); + } + + + public static boolean exampleSharedFlag() { + return FEATURE_FLAGS.exampleSharedFlag(); } - - + + + + public static boolean extendedWallpaperEffects() { + + return FEATURE_FLAGS.extendedWallpaperEffects(); + } + + + + public static boolean lockscreenCustomClocks() { + + return FEATURE_FLAGS.lockscreenCustomClocks(); + } + + + + public static boolean newCustomizationPickerUi() { + + return FEATURE_FLAGS.newCustomizationPickerUi(); + } + + + + public static boolean newTouchpadGesturesTutorial() { + + return FEATURE_FLAGS.newTouchpadGesturesTutorial(); + } + + + public static boolean returnAnimationFrameworkLibrary() { + return FEATURE_FLAGS.returnAnimationFrameworkLibrary(); } - - + + + + public static boolean returnAnimationFrameworkLongLived() { + + return FEATURE_FLAGS.returnAnimationFrameworkLongLived(); + } + + + + public static boolean screenshotContextUrl() { + + return FEATURE_FLAGS.screenshotContextUrl(); + } + + + public static boolean shadeAllowBackGesture() { + return FEATURE_FLAGS.shadeAllowBackGesture(); } - - + + + public static boolean sidefpsControllerRefactor() { + return FEATURE_FLAGS.sidefpsControllerRefactor(); } + + + public static boolean smartspaceRemoteviewsIntentHandler() { + + return FEATURE_FLAGS.smartspaceRemoteviewsIntentHandler(); + } + + + + public static boolean smartspaceSportsCardBackground() { + + return FEATURE_FLAGS.smartspaceSportsCardBackground(); + } + + + + public static boolean smartspaceUiUpdate() { + + return FEATURE_FLAGS.smartspaceUiUpdate(); + } + + + + public static boolean smartspaceUiUpdateResources() { + + return FEATURE_FLAGS.smartspaceUiUpdateResources(); + } + + + + public static boolean statusBarConnectedDisplays() { + + return FEATURE_FLAGS.statusBarConnectedDisplays(); + } + + + + public static boolean threeButtonCornerSwipe() { + + return FEATURE_FLAGS.threeButtonCornerSwipe(); + } + + + + public static boolean usePreferredImageEditor() { + + return FEATURE_FLAGS.usePreferredImageEditor(); + } + private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); } diff --git a/flags/src/com/android/window/flags2/CustomFeatureFlags.java b/flags/src/com/android/window/flags2/CustomFeatureFlags.java index c52b5a4bb9..3591e0549a 100644 --- a/flags/src/com/android/window/flags2/CustomFeatureFlags.java +++ b/flags/src/com/android/window/flags2/CustomFeatureFlags.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Predicate; - /** @hide */ public class CustomFeatureFlags implements FeatureFlags { @@ -17,742 +16,1870 @@ public class CustomFeatureFlags implements FeatureFlags { mGetValueImpl = getValueImpl; } @Override - + + public boolean actionModeEdgeToEdge() { + return getValue(Flags.FLAG_ACTION_MODE_EDGE_TO_EDGE, + FeatureFlags::actionModeEdgeToEdge); + } + + @Override + public boolean activityEmbeddingAnimationCustomizationFlag() { return getValue(Flags.FLAG_ACTIVITY_EMBEDDING_ANIMATION_CUSTOMIZATION_FLAG, - FeatureFlags::activityEmbeddingAnimationCustomizationFlag); + FeatureFlags::activityEmbeddingAnimationCustomizationFlag); } @Override - + + public boolean activityEmbeddingDelayTaskFragmentFinishForActivityLaunch() { + return getValue(Flags.FLAG_ACTIVITY_EMBEDDING_DELAY_TASK_FRAGMENT_FINISH_FOR_ACTIVITY_LAUNCH, + FeatureFlags::activityEmbeddingDelayTaskFragmentFinishForActivityLaunch); + } + + @Override + public boolean activityEmbeddingInteractiveDividerFlag() { return getValue(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG, - FeatureFlags::activityEmbeddingInteractiveDividerFlag); + FeatureFlags::activityEmbeddingInteractiveDividerFlag); } @Override - - public boolean activityEmbeddingOverlayPresentationFlag() { - return getValue(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG, - FeatureFlags::activityEmbeddingOverlayPresentationFlag); + + public boolean activityEmbeddingMetrics() { + return getValue(Flags.FLAG_ACTIVITY_EMBEDDING_METRICS, + FeatureFlags::activityEmbeddingMetrics); } @Override - - public boolean activitySnapshotByDefault() { - return getValue(Flags.FLAG_ACTIVITY_SNAPSHOT_BY_DEFAULT, - FeatureFlags::activitySnapshotByDefault); + + public boolean activityEmbeddingSupportForConnectedDisplays() { + return getValue(Flags.FLAG_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS, + FeatureFlags::activityEmbeddingSupportForConnectedDisplays); } @Override - - public boolean activityWindowInfoFlag() { - return getValue(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG, - FeatureFlags::activityWindowInfoFlag); - } - @Override - public boolean allowDisableActivityRecordInputSink() { return getValue(Flags.FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK, - FeatureFlags::allowDisableActivityRecordInputSink); + FeatureFlags::allowDisableActivityRecordInputSink); } @Override - + public boolean allowHideScmButton() { return getValue(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON, - FeatureFlags::allowHideScmButton); + FeatureFlags::allowHideScmButton); } @Override - + public boolean allowsScreenSizeDecoupledFromStatusBarAndCutout() { return getValue(Flags.FLAG_ALLOWS_SCREEN_SIZE_DECOUPLED_FROM_STATUS_BAR_AND_CUTOUT, - FeatureFlags::allowsScreenSizeDecoupledFromStatusBarAndCutout); + FeatureFlags::allowsScreenSizeDecoupledFromStatusBarAndCutout); } @Override - - public boolean alwaysDeferTransitionWhenApplyWct() { - return getValue(Flags.FLAG_ALWAYS_DEFER_TRANSITION_WHEN_APPLY_WCT, - FeatureFlags::alwaysDeferTransitionWhenApplyWct); - } - @Override - public boolean alwaysDrawMagnificationFullscreenBorder() { return getValue(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER, - FeatureFlags::alwaysDrawMagnificationFullscreenBorder); + FeatureFlags::alwaysDrawMagnificationFullscreenBorder); } @Override - + public boolean alwaysUpdateWallpaperPermission() { return getValue(Flags.FLAG_ALWAYS_UPDATE_WALLPAPER_PERMISSION, - FeatureFlags::alwaysUpdateWallpaperPermission); + FeatureFlags::alwaysUpdateWallpaperPermission); } @Override - + + public boolean aodTransition() { + return getValue(Flags.FLAG_AOD_TRANSITION, + FeatureFlags::aodTransition); + } + + @Override + + public boolean appCompatAsyncRelayout() { + return getValue(Flags.FLAG_APP_COMPAT_ASYNC_RELAYOUT, + FeatureFlags::appCompatAsyncRelayout); + } + + @Override + public boolean appCompatPropertiesApi() { return getValue(Flags.FLAG_APP_COMPAT_PROPERTIES_API, - FeatureFlags::appCompatPropertiesApi); + FeatureFlags::appCompatPropertiesApi); } @Override - + public boolean appCompatRefactoring() { return getValue(Flags.FLAG_APP_COMPAT_REFACTORING, - FeatureFlags::appCompatRefactoring); + FeatureFlags::appCompatRefactoring); } @Override - + + public boolean appCompatUiFramework() { + return getValue(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK, + FeatureFlags::appCompatUiFramework); + } + + @Override + + public boolean appHandleNoRelayoutOnExclusionChange() { + return getValue(Flags.FLAG_APP_HANDLE_NO_RELAYOUT_ON_EXCLUSION_CHANGE, + FeatureFlags::appHandleNoRelayoutOnExclusionChange); + } + + @Override + + public boolean applyLifecycleOnPipChange() { + return getValue(Flags.FLAG_APPLY_LIFECYCLE_ON_PIP_CHANGE, + FeatureFlags::applyLifecycleOnPipChange); + } + + @Override + + public boolean avoidRebindingIntentionallyDisconnectedWallpaper() { + return getValue(Flags.FLAG_AVOID_REBINDING_INTENTIONALLY_DISCONNECTED_WALLPAPER, + FeatureFlags::avoidRebindingIntentionallyDisconnectedWallpaper); + } + + @Override + + public boolean backupAndRestoreForUserAspectRatioSettings() { + return getValue(Flags.FLAG_BACKUP_AND_RESTORE_FOR_USER_ASPECT_RATIO_SETTINGS, + FeatureFlags::backupAndRestoreForUserAspectRatioSettings); + } + + @Override + + public boolean balAdditionalLogging() { + return getValue(Flags.FLAG_BAL_ADDITIONAL_LOGGING, + FeatureFlags::balAdditionalLogging); + } + + @Override + + public boolean balAdditionalStartModes() { + return getValue(Flags.FLAG_BAL_ADDITIONAL_START_MODES, + FeatureFlags::balAdditionalStartModes); + } + + @Override + + public boolean balClearAllowlistDuration() { + return getValue(Flags.FLAG_BAL_CLEAR_ALLOWLIST_DURATION, + FeatureFlags::balClearAllowlistDuration); + } + + @Override + public boolean balDontBringExistingBackgroundTaskStackToFg() { return getValue(Flags.FLAG_BAL_DONT_BRING_EXISTING_BACKGROUND_TASK_STACK_TO_FG, - FeatureFlags::balDontBringExistingBackgroundTaskStackToFg); + FeatureFlags::balDontBringExistingBackgroundTaskStackToFg); } @Override - + public boolean balImproveRealCallerVisibilityCheck() { return getValue(Flags.FLAG_BAL_IMPROVE_REAL_CALLER_VISIBILITY_CHECK, - FeatureFlags::balImproveRealCallerVisibilityCheck); + FeatureFlags::balImproveRealCallerVisibilityCheck); } @Override - + public boolean balImprovedMetrics() { return getValue(Flags.FLAG_BAL_IMPROVED_METRICS, - FeatureFlags::balImprovedMetrics); + FeatureFlags::balImprovedMetrics); } @Override - + + public boolean balReduceGracePeriod() { + return getValue(Flags.FLAG_BAL_REDUCE_GRACE_PERIOD, + FeatureFlags::balReduceGracePeriod); + } + + @Override + public boolean balRequireOptInByPendingIntentCreator() { return getValue(Flags.FLAG_BAL_REQUIRE_OPT_IN_BY_PENDING_INTENT_CREATOR, - FeatureFlags::balRequireOptInByPendingIntentCreator); + FeatureFlags::balRequireOptInByPendingIntentCreator); } @Override - - public boolean balRequireOptInSameUid() { - return getValue(Flags.FLAG_BAL_REQUIRE_OPT_IN_SAME_UID, - FeatureFlags::balRequireOptInSameUid); - } - @Override - public boolean balRespectAppSwitchStateWhenCheckBoundByForegroundUid() { return getValue(Flags.FLAG_BAL_RESPECT_APP_SWITCH_STATE_WHEN_CHECK_BOUND_BY_FOREGROUND_UID, - FeatureFlags::balRespectAppSwitchStateWhenCheckBoundByForegroundUid); + FeatureFlags::balRespectAppSwitchStateWhenCheckBoundByForegroundUid); } @Override - - public boolean balShowToasts() { - return getValue(Flags.FLAG_BAL_SHOW_TOASTS, - FeatureFlags::balShowToasts); + + public boolean balSendIntentWithOptions() { + return getValue(Flags.FLAG_BAL_SEND_INTENT_WITH_OPTIONS, + FeatureFlags::balSendIntentWithOptions); } @Override - + public boolean balShowToastsBlocked() { return getValue(Flags.FLAG_BAL_SHOW_TOASTS_BLOCKED, - FeatureFlags::balShowToastsBlocked); + FeatureFlags::balShowToastsBlocked); } @Override - - public boolean blastSyncNotificationShadeOnDisplaySwitch() { - return getValue(Flags.FLAG_BLAST_SYNC_NOTIFICATION_SHADE_ON_DISPLAY_SWITCH, - FeatureFlags::blastSyncNotificationShadeOnDisplaySwitch); + + public boolean balStrictModeGracePeriod() { + return getValue(Flags.FLAG_BAL_STRICT_MODE_GRACE_PERIOD, + FeatureFlags::balStrictModeGracePeriod); } @Override - - public boolean bundleClientTransactionFlag() { - return getValue(Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG, - FeatureFlags::bundleClientTransactionFlag); + + public boolean balStrictModeRo() { + return getValue(Flags.FLAG_BAL_STRICT_MODE_RO, + FeatureFlags::balStrictModeRo); } @Override - + + public boolean betterSupportNonMatchParentActivity() { + return getValue(Flags.FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY, + FeatureFlags::betterSupportNonMatchParentActivity); + } + + @Override + + public boolean cacheWindowStyle() { + return getValue(Flags.FLAG_CACHE_WINDOW_STYLE, + FeatureFlags::cacheWindowStyle); + } + + @Override + public boolean cameraCompatForFreeform() { return getValue(Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM, - FeatureFlags::cameraCompatForFreeform); + FeatureFlags::cameraCompatForFreeform); } @Override - + + public boolean cameraCompatFullscreenPickSameTaskActivity() { + return getValue(Flags.FLAG_CAMERA_COMPAT_FULLSCREEN_PICK_SAME_TASK_ACTIVITY, + FeatureFlags::cameraCompatFullscreenPickSameTaskActivity); + } + + @Override + + public boolean checkDisabledSnapshotsInTaskPersister() { + return getValue(Flags.FLAG_CHECK_DISABLED_SNAPSHOTS_IN_TASK_PERSISTER, + FeatureFlags::checkDisabledSnapshotsInTaskPersister); + } + + @Override + + public boolean cleanupDispatchPendingTransactionsRemoteException() { + return getValue(Flags.FLAG_CLEANUP_DISPATCH_PENDING_TRANSACTIONS_REMOTE_EXCEPTION, + FeatureFlags::cleanupDispatchPendingTransactionsRemoteException); + } + + @Override + + public boolean clearSystemVibrator() { + return getValue(Flags.FLAG_CLEAR_SYSTEM_VIBRATOR, + FeatureFlags::clearSystemVibrator); + } + + @Override + public boolean closeToSquareConfigIncludesStatusBar() { return getValue(Flags.FLAG_CLOSE_TO_SQUARE_CONFIG_INCLUDES_STATUS_BAR, - FeatureFlags::closeToSquareConfigIncludesStatusBar); + FeatureFlags::closeToSquareConfigIncludesStatusBar); } @Override - + + public boolean condenseConfigurationChangeForSimpleMode() { + return getValue(Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE, + FeatureFlags::condenseConfigurationChangeForSimpleMode); + } + + @Override + public boolean configurableFontScaleDefault() { return getValue(Flags.FLAG_CONFIGURABLE_FONT_SCALE_DEFAULT, - FeatureFlags::configurableFontScaleDefault); + FeatureFlags::configurableFontScaleDefault); } @Override - + public boolean coverDisplayOptIn() { return getValue(Flags.FLAG_COVER_DISPLAY_OPT_IN, - FeatureFlags::coverDisplayOptIn); + FeatureFlags::coverDisplayOptIn); } @Override - - public boolean deferDisplayUpdates() { - return getValue(Flags.FLAG_DEFER_DISPLAY_UPDATES, - FeatureFlags::deferDisplayUpdates); - } - @Override - public boolean delayNotificationToMagnificationWhenRecentsWindowToFrontTransition() { return getValue(Flags.FLAG_DELAY_NOTIFICATION_TO_MAGNIFICATION_WHEN_RECENTS_WINDOW_TO_FRONT_TRANSITION, - FeatureFlags::delayNotificationToMagnificationWhenRecentsWindowToFrontTransition); + FeatureFlags::delayNotificationToMagnificationWhenRecentsWindowToFrontTransition); } @Override - + + public boolean delegateBackGestureToShell() { + return getValue(Flags.FLAG_DELEGATE_BACK_GESTURE_TO_SHELL, + FeatureFlags::delegateBackGestureToShell); + } + + @Override + public boolean delegateUnhandledDrags() { return getValue(Flags.FLAG_DELEGATE_UNHANDLED_DRAGS, - FeatureFlags::delegateUnhandledDrags); + FeatureFlags::delegateUnhandledDrags); } @Override - + public boolean deleteCaptureDisplay() { return getValue(Flags.FLAG_DELETE_CAPTURE_DISPLAY, - FeatureFlags::deleteCaptureDisplay); + FeatureFlags::deleteCaptureDisplay); } @Override - + public boolean density390Api() { return getValue(Flags.FLAG_DENSITY_390_API, - FeatureFlags::density390Api); + FeatureFlags::density390Api); } @Override - - public boolean disableObjectPool() { - return getValue(Flags.FLAG_DISABLE_OBJECT_POOL, - FeatureFlags::disableObjectPool); + + public boolean disableDesktopLaunchParamsOutsideDesktopBugFix() { + return getValue(Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX, + FeatureFlags::disableDesktopLaunchParamsOutsideDesktopBugFix); } @Override - - public boolean disableThinLetterboxingPolicy() { - return getValue(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY, - FeatureFlags::disableThinLetterboxingPolicy); + + public boolean disableNonResizableAppSnapResizing() { + return getValue(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, + FeatureFlags::disableNonResizableAppSnapResizing); } @Override - + + public boolean disableOptOutEdgeToEdge() { + return getValue(Flags.FLAG_DISABLE_OPT_OUT_EDGE_TO_EDGE, + FeatureFlags::disableOptOutEdgeToEdge); + } + + @Override + public boolean doNotCheckIntersectionWhenNonMagnifiableWindowTransitions() { return getValue(Flags.FLAG_DO_NOT_CHECK_INTERSECTION_WHEN_NON_MAGNIFIABLE_WINDOW_TRANSITIONS, - FeatureFlags::doNotCheckIntersectionWhenNonMagnifiableWindowTransitions); + FeatureFlags::doNotCheckIntersectionWhenNonMagnifiableWindowTransitions); } @Override - - public boolean drawSnapshotAspectRatioMatch() { - return getValue(Flags.FLAG_DRAW_SNAPSHOT_ASPECT_RATIO_MATCH, - FeatureFlags::drawSnapshotAspectRatioMatch); + + public boolean earlyLaunchHint() { + return getValue(Flags.FLAG_EARLY_LAUNCH_HINT, + FeatureFlags::earlyLaunchHint); } @Override - + public boolean edgeToEdgeByDefault() { return getValue(Flags.FLAG_EDGE_TO_EDGE_BY_DEFAULT, - FeatureFlags::edgeToEdgeByDefault); + FeatureFlags::edgeToEdgeByDefault); } @Override - - public boolean embeddedActivityBackNavFlag() { - return getValue(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG, - FeatureFlags::embeddedActivityBackNavFlag); + + public boolean enableAccessibleCustomHeaders() { + return getValue(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS, + FeatureFlags::enableAccessibleCustomHeaders); } @Override - - public boolean enableAdditionalWindowsAboveStatusBar() { - return getValue(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR, - FeatureFlags::enableAdditionalWindowsAboveStatusBar); + + public boolean enableActivityEmbeddingSupportForConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS, + FeatureFlags::enableActivityEmbeddingSupportForConnectedDisplays); } @Override - + public boolean enableAppHeaderWithTaskDensity() { return getValue(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY, - FeatureFlags::enableAppHeaderWithTaskDensity); + FeatureFlags::enableAppHeaderWithTaskDensity); } @Override - + + public boolean enableBorderSettings() { + return getValue(Flags.FLAG_ENABLE_BORDER_SETTINGS, + FeatureFlags::enableBorderSettings); + } + + @Override + public boolean enableBufferTransformHintFromDisplay() { return getValue(Flags.FLAG_ENABLE_BUFFER_TRANSFORM_HINT_FROM_DISPLAY, - FeatureFlags::enableBufferTransformHintFromDisplay); + FeatureFlags::enableBufferTransformHintFromDisplay); } @Override - + + public boolean enableBugFixesForSecondaryDisplay() { + return getValue(Flags.FLAG_ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY, + FeatureFlags::enableBugFixesForSecondaryDisplay); + } + + @Override + public boolean enableCameraCompatForDesktopWindowing() { return getValue(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, - FeatureFlags::enableCameraCompatForDesktopWindowing); + FeatureFlags::enableCameraCompatForDesktopWindowing); } @Override - + + public boolean enableCameraCompatForDesktopWindowingOptOut() { + return getValue(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT, + FeatureFlags::enableCameraCompatForDesktopWindowingOptOut); + } + + @Override + + public boolean enableCameraCompatForDesktopWindowingOptOutApi() { + return getValue(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT_API, + FeatureFlags::enableCameraCompatForDesktopWindowingOptOutApi); + } + + @Override + + public boolean enableCameraCompatTrackTaskAndAppBugfix() { + return getValue(Flags.FLAG_ENABLE_CAMERA_COMPAT_TRACK_TASK_AND_APP_BUGFIX, + FeatureFlags::enableCameraCompatTrackTaskAndAppBugfix); + } + + @Override + + public boolean enableCaptionCompatInsetConversion() { + return getValue(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_CONVERSION, + FeatureFlags::enableCaptionCompatInsetConversion); + } + + @Override + + public boolean enableCaptionCompatInsetForceConsumption() { + return getValue(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION, + FeatureFlags::enableCaptionCompatInsetForceConsumption); + } + + @Override + + public boolean enableCaptionCompatInsetForceConsumptionAlways() { + return getValue(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS, + FeatureFlags::enableCaptionCompatInsetForceConsumptionAlways); + } + + @Override + + public boolean enableCascadingWindows() { + return getValue(Flags.FLAG_ENABLE_CASCADING_WINDOWS, + FeatureFlags::enableCascadingWindows); + } + + @Override + + public boolean enableCompatUiVisibilityStatus() { + return getValue(Flags.FLAG_ENABLE_COMPAT_UI_VISIBILITY_STATUS, + FeatureFlags::enableCompatUiVisibilityStatus); + } + + @Override + public boolean enableCompatuiSysuiLauncher() { return getValue(Flags.FLAG_ENABLE_COMPATUI_SYSUI_LAUNCHER, - FeatureFlags::enableCompatuiSysuiLauncher); + FeatureFlags::enableCompatuiSysuiLauncher); } @Override - + + public boolean enableConnectedDisplaysDnd() { + return getValue(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND, + FeatureFlags::enableConnectedDisplaysDnd); + } + + @Override + + public boolean enableConnectedDisplaysPip() { + return getValue(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP, + FeatureFlags::enableConnectedDisplaysPip); + } + + @Override + + public boolean enableConnectedDisplaysWindowDrag() { + return getValue(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG, + FeatureFlags::enableConnectedDisplaysWindowDrag); + } + + @Override + + public boolean enableDesktopAppHandleAnimation() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_APP_HANDLE_ANIMATION, + FeatureFlags::enableDesktopAppHandleAnimation); + } + + @Override + + public boolean enableDesktopAppLaunchAlttabTransitions() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS, + FeatureFlags::enableDesktopAppLaunchAlttabTransitions); + } + + @Override + + public boolean enableDesktopAppLaunchAlttabTransitionsBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX, + FeatureFlags::enableDesktopAppLaunchAlttabTransitionsBugfix); + } + + @Override + + public boolean enableDesktopAppLaunchTransitions() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, + FeatureFlags::enableDesktopAppLaunchTransitions); + } + + @Override + + public boolean enableDesktopAppLaunchTransitionsBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + FeatureFlags::enableDesktopAppLaunchTransitionsBugfix); + } + + @Override + + public boolean enableDesktopCloseShortcutBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX, + FeatureFlags::enableDesktopCloseShortcutBugfix); + } + + @Override + + public boolean enableDesktopCloseTaskAnimationInDtcBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_CLOSE_TASK_ANIMATION_IN_DTC_BUGFIX, + FeatureFlags::enableDesktopCloseTaskAnimationInDtcBugfix); + } + + @Override + + public boolean enableDesktopImeBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_IME_BUGFIX, + FeatureFlags::enableDesktopImeBugfix); + } + + @Override + + public boolean enableDesktopImmersiveDragBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX, + FeatureFlags::enableDesktopImmersiveDragBugfix); + } + + @Override + + public boolean enableDesktopIndicatorInSeparateThreadBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX, + FeatureFlags::enableDesktopIndicatorInSeparateThreadBugfix); + } + + @Override + + public boolean enableDesktopModeThroughDevOption() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION, + FeatureFlags::enableDesktopModeThroughDevOption); + } + + @Override + + public boolean enableDesktopOpeningDeeplinkMinimizeAnimationBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX, + FeatureFlags::enableDesktopOpeningDeeplinkMinimizeAnimationBugfix); + } + + @Override + + public boolean enableDesktopRecentsTransitionsCornersBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX, + FeatureFlags::enableDesktopRecentsTransitionsCornersBugfix); + } + + @Override + + public boolean enableDesktopSwipeBackMinimizeAnimationBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_SWIPE_BACK_MINIMIZE_ANIMATION_BUGFIX, + FeatureFlags::enableDesktopSwipeBackMinimizeAnimationBugfix); + } + + @Override + + public boolean enableDesktopSystemDialogsTransitions() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS, + FeatureFlags::enableDesktopSystemDialogsTransitions); + } + + @Override + + public boolean enableDesktopTabTearingMinimizeAnimationBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX, + FeatureFlags::enableDesktopTabTearingMinimizeAnimationBugfix); + } + + @Override + + public boolean enableDesktopTaskbarOnFreeformDisplays() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_TASKBAR_ON_FREEFORM_DISPLAYS, + FeatureFlags::enableDesktopTaskbarOnFreeformDisplays); + } + + @Override + + public boolean enableDesktopTrampolineCloseAnimationBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX, + FeatureFlags::enableDesktopTrampolineCloseAnimationBugfix); + } + + @Override + + public boolean enableDesktopWallpaperActivityForSystemUser() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + FeatureFlags::enableDesktopWallpaperActivityForSystemUser); + } + + @Override + + public boolean enableDesktopWindowingAppHandleEducation() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION, + FeatureFlags::enableDesktopWindowingAppHandleEducation); + } + + @Override + + public boolean enableDesktopWindowingAppToWeb() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB, + FeatureFlags::enableDesktopWindowingAppToWeb); + } + + @Override + + public boolean enableDesktopWindowingAppToWebEducation() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION, + FeatureFlags::enableDesktopWindowingAppToWebEducation); + } + + @Override + + public boolean enableDesktopWindowingAppToWebEducationIntegration() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION_INTEGRATION, + FeatureFlags::enableDesktopWindowingAppToWebEducationIntegration); + } + + @Override + + public boolean enableDesktopWindowingBackNavigation() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + FeatureFlags::enableDesktopWindowingBackNavigation); + } + + @Override + + public boolean enableDesktopWindowingEnterTransitionBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITION_BUGFIX, + FeatureFlags::enableDesktopWindowingEnterTransitionBugfix); + } + + @Override + + public boolean enableDesktopWindowingEnterTransitions() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS, + FeatureFlags::enableDesktopWindowingEnterTransitions); + } + + @Override + + public boolean enableDesktopWindowingExitByMinimizeTransitionBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX, + FeatureFlags::enableDesktopWindowingExitByMinimizeTransitionBugfix); + } + + @Override + + public boolean enableDesktopWindowingExitTransitions() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS, + FeatureFlags::enableDesktopWindowingExitTransitions); + } + + @Override + + public boolean enableDesktopWindowingExitTransitionsBugfix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX, + FeatureFlags::enableDesktopWindowingExitTransitionsBugfix); + } + + @Override + + public boolean enableDesktopWindowingHsum() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_HSUM, + FeatureFlags::enableDesktopWindowingHsum); + } + + @Override + public boolean enableDesktopWindowingImmersiveHandleHiding() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING, - FeatureFlags::enableDesktopWindowingImmersiveHandleHiding); + FeatureFlags::enableDesktopWindowingImmersiveHandleHiding); } @Override - + public boolean enableDesktopWindowingModalsPolicy() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, - FeatureFlags::enableDesktopWindowingModalsPolicy); + FeatureFlags::enableDesktopWindowingModalsPolicy); } @Override - + public boolean enableDesktopWindowingMode() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - FeatureFlags::enableDesktopWindowingMode); + FeatureFlags::enableDesktopWindowingMode); } @Override - + + public boolean enableDesktopWindowingMultiInstanceFeatures() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES, + FeatureFlags::enableDesktopWindowingMultiInstanceFeatures); + } + + @Override + + public boolean enableDesktopWindowingPersistence() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, + FeatureFlags::enableDesktopWindowingPersistence); + } + + @Override + + public boolean enableDesktopWindowingPip() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + FeatureFlags::enableDesktopWindowingPip); + } + + @Override + public boolean enableDesktopWindowingQuickSwitch() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH, - FeatureFlags::enableDesktopWindowingQuickSwitch); + FeatureFlags::enableDesktopWindowingQuickSwitch); } @Override - - public boolean enableDesktopWindowingScvhCache() { - return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE, - FeatureFlags::enableDesktopWindowingScvhCache); + + public boolean enableDesktopWindowingScvhCacheBugFix() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE_BUG_FIX, + FeatureFlags::enableDesktopWindowingScvhCacheBugFix); } @Override - + public boolean enableDesktopWindowingSizeConstraints() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS, - FeatureFlags::enableDesktopWindowingSizeConstraints); + FeatureFlags::enableDesktopWindowingSizeConstraints); } @Override - + public boolean enableDesktopWindowingTaskLimit() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASK_LIMIT, - FeatureFlags::enableDesktopWindowingTaskLimit); + FeatureFlags::enableDesktopWindowingTaskLimit); } @Override - + public boolean enableDesktopWindowingTaskbarRunningApps() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, - FeatureFlags::enableDesktopWindowingTaskbarRunningApps); + FeatureFlags::enableDesktopWindowingTaskbarRunningApps); } @Override - + + public boolean enableDesktopWindowingTransitions() { + return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS, + FeatureFlags::enableDesktopWindowingTransitions); + } + + @Override + public boolean enableDesktopWindowingWallpaperActivity() { return getValue(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - FeatureFlags::enableDesktopWindowingWallpaperActivity); + FeatureFlags::enableDesktopWindowingWallpaperActivity); } @Override - - public boolean enableScaledResizing() { - return getValue(Flags.FLAG_ENABLE_SCALED_RESIZING, - FeatureFlags::enableScaledResizing); + + public boolean enableDeviceStateAutoRotateSettingLogging() { + return getValue(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING, + FeatureFlags::enableDeviceStateAutoRotateSettingLogging); } @Override - + + public boolean enableDeviceStateAutoRotateSettingRefactor() { + return getValue(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR, + FeatureFlags::enableDeviceStateAutoRotateSettingRefactor); + } + + @Override + + public boolean enableDisplayDisconnectInteraction() { + return getValue(Flags.FLAG_ENABLE_DISPLAY_DISCONNECT_INTERACTION, + FeatureFlags::enableDisplayDisconnectInteraction); + } + + @Override + + public boolean enableDisplayFocusInShellTransitions() { + return getValue(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS, + FeatureFlags::enableDisplayFocusInShellTransitions); + } + + @Override + + public boolean enableDisplayReconnectInteraction() { + return getValue(Flags.FLAG_ENABLE_DISPLAY_RECONNECT_INTERACTION, + FeatureFlags::enableDisplayReconnectInteraction); + } + + @Override + + public boolean enableDisplayWindowingModeSwitching() { + return getValue(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING, + FeatureFlags::enableDisplayWindowingModeSwitching); + } + + @Override + + public boolean enableDragResizeSetUpInBgThread() { + return getValue(Flags.FLAG_ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD, + FeatureFlags::enableDragResizeSetUpInBgThread); + } + + @Override + + public boolean enableDragToDesktopIncomingTransitionsBugfix() { + return getValue(Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX, + FeatureFlags::enableDragToDesktopIncomingTransitionsBugfix); + } + + @Override + + public boolean enableDragToMaximize() { + return getValue(Flags.FLAG_ENABLE_DRAG_TO_MAXIMIZE, + FeatureFlags::enableDragToMaximize); + } + + @Override + + public boolean enableDynamicRadiusComputationBugfix() { + return getValue(Flags.FLAG_ENABLE_DYNAMIC_RADIUS_COMPUTATION_BUGFIX, + FeatureFlags::enableDynamicRadiusComputationBugfix); + } + + @Override + + public boolean enableFullScreenWindowOnRemovingSplitScreenStageBugfix() { + return getValue(Flags.FLAG_ENABLE_FULL_SCREEN_WINDOW_ON_REMOVING_SPLIT_SCREEN_STAGE_BUGFIX, + FeatureFlags::enableFullScreenWindowOnRemovingSplitScreenStageBugfix); + } + + @Override + + public boolean enableFullyImmersiveInDesktop() { + return getValue(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, + FeatureFlags::enableFullyImmersiveInDesktop); + } + + @Override + + public boolean enableHandleInputFix() { + return getValue(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX, + FeatureFlags::enableHandleInputFix); + } + + @Override + + public boolean enableHoldToDragAppHandle() { + return getValue(Flags.FLAG_ENABLE_HOLD_TO_DRAG_APP_HANDLE, + FeatureFlags::enableHoldToDragAppHandle); + } + + @Override + + public boolean enableInputLayerTransitionFix() { + return getValue(Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX, + FeatureFlags::enableInputLayerTransitionFix); + } + + @Override + + public boolean enableMinimizeButton() { + return getValue(Flags.FLAG_ENABLE_MINIMIZE_BUTTON, + FeatureFlags::enableMinimizeButton); + } + + @Override + + public boolean enableModalsFullscreenWithPermission() { + return getValue(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION, + FeatureFlags::enableModalsFullscreenWithPermission); + } + + @Override + + public boolean enableMoveToNextDisplayShortcut() { + return getValue(Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, + FeatureFlags::enableMoveToNextDisplayShortcut); + } + + @Override + + public boolean enableMultiDisplaySplit() { + return getValue(Flags.FLAG_ENABLE_MULTI_DISPLAY_SPLIT, + FeatureFlags::enableMultiDisplaySplit); + } + + @Override + + public boolean enableMultidisplayTrackpadBackGesture() { + return getValue(Flags.FLAG_ENABLE_MULTIDISPLAY_TRACKPAD_BACK_GESTURE, + FeatureFlags::enableMultidisplayTrackpadBackGesture); + } + + @Override + + public boolean enableMultipleDesktopsBackend() { + return getValue(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + FeatureFlags::enableMultipleDesktopsBackend); + } + + @Override + + public boolean enableMultipleDesktopsFrontend() { + return getValue(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND, + FeatureFlags::enableMultipleDesktopsFrontend); + } + + @Override + + public boolean enableNonDefaultDisplaySplit() { + return getValue(Flags.FLAG_ENABLE_NON_DEFAULT_DISPLAY_SPLIT, + FeatureFlags::enableNonDefaultDisplaySplit); + } + + @Override + + public boolean enableOpaqueBackgroundForTransparentWindows() { + return getValue(Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS, + FeatureFlags::enableOpaqueBackgroundForTransparentWindows); + } + + @Override + + public boolean enablePerDisplayDesktopWallpaperActivity() { + return getValue(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + FeatureFlags::enablePerDisplayDesktopWallpaperActivity); + } + + @Override + + public boolean enablePerDisplayPackageContextCacheInStatusbarNotif() { + return getValue(Flags.FLAG_ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF, + FeatureFlags::enablePerDisplayPackageContextCacheInStatusbarNotif); + } + + @Override + + public boolean enablePersistingDisplaySizeForConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS, + FeatureFlags::enablePersistingDisplaySizeForConnectedDisplays); + } + + @Override + + public boolean enablePresentationForConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS, + FeatureFlags::enablePresentationForConnectedDisplays); + } + + @Override + + public boolean enableProjectedDisplayDesktopMode() { + return getValue(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE, + FeatureFlags::enableProjectedDisplayDesktopMode); + } + + @Override + + public boolean enableQuickswitchDesktopSplitBugfix() { + return getValue(Flags.FLAG_ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX, + FeatureFlags::enableQuickswitchDesktopSplitBugfix); + } + + @Override + + public boolean enableRequestFullscreenBugfix() { + return getValue(Flags.FLAG_ENABLE_REQUEST_FULLSCREEN_BUGFIX, + FeatureFlags::enableRequestFullscreenBugfix); + } + + @Override + + public boolean enableResizingMetrics() { + return getValue(Flags.FLAG_ENABLE_RESIZING_METRICS, + FeatureFlags::enableResizingMetrics); + } + + @Override + + public boolean enableRestartMenuForConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS, + FeatureFlags::enableRestartMenuForConnectedDisplays); + } + + @Override + + public boolean enableRestoreToPreviousSizeFromDesktopImmersive() { + return getValue(Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE, + FeatureFlags::enableRestoreToPreviousSizeFromDesktopImmersive); + } + + @Override + + public boolean enableShellInitialBoundsRegressionBugFix() { + return getValue(Flags.FLAG_ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX, + FeatureFlags::enableShellInitialBoundsRegressionBugFix); + } + + @Override + + public boolean enableSizeCompatModeImprovementsForConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_SIZE_COMPAT_MODE_IMPROVEMENTS_FOR_CONNECTED_DISPLAYS, + FeatureFlags::enableSizeCompatModeImprovementsForConnectedDisplays); + } + + @Override + + public boolean enableStartLaunchTransitionFromTaskbarBugfix() { + return getValue(Flags.FLAG_ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX, + FeatureFlags::enableStartLaunchTransitionFromTaskbarBugfix); + } + + @Override + + public boolean enableTaskResizingKeyboardShortcuts() { + return getValue(Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS, + FeatureFlags::enableTaskResizingKeyboardShortcuts); + } + + @Override + public boolean enableTaskStackObserverInShell() { return getValue(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL, - FeatureFlags::enableTaskStackObserverInShell); + FeatureFlags::enableTaskStackObserverInShell); } @Override - + + public boolean enableTaskbarConnectedDisplays() { + return getValue(Flags.FLAG_ENABLE_TASKBAR_CONNECTED_DISPLAYS, + FeatureFlags::enableTaskbarConnectedDisplays); + } + + @Override + + public boolean enableTaskbarOverflow() { + return getValue(Flags.FLAG_ENABLE_TASKBAR_OVERFLOW, + FeatureFlags::enableTaskbarOverflow); + } + + @Override + + public boolean enableTaskbarRecentsLayoutTransition() { + return getValue(Flags.FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION, + FeatureFlags::enableTaskbarRecentsLayoutTransition); + } + + @Override + public boolean enableThemedAppHeaders() { return getValue(Flags.FLAG_ENABLE_THEMED_APP_HEADERS, - FeatureFlags::enableThemedAppHeaders); + FeatureFlags::enableThemedAppHeaders); } @Override - + + public boolean enableTileResizing() { + return getValue(Flags.FLAG_ENABLE_TILE_RESIZING, + FeatureFlags::enableTileResizing); + } + + @Override + + public boolean enableTopVisibleRootTaskPerUserTracking() { + return getValue(Flags.FLAG_ENABLE_TOP_VISIBLE_ROOT_TASK_PER_USER_TRACKING, + FeatureFlags::enableTopVisibleRootTaskPerUserTracking); + } + + @Override + + public boolean enableVisualIndicatorInTransitionBugfix() { + return getValue(Flags.FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX, + FeatureFlags::enableVisualIndicatorInTransitionBugfix); + } + + @Override + + public boolean enableWindowContextResourcesUpdateOnConfigChange() { + return getValue(Flags.FLAG_ENABLE_WINDOW_CONTEXT_RESOURCES_UPDATE_ON_CONFIG_CHANGE, + FeatureFlags::enableWindowContextResourcesUpdateOnConfigChange); + } + + @Override + public boolean enableWindowingDynamicInitialBounds() { return getValue(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, - FeatureFlags::enableWindowingDynamicInitialBounds); + FeatureFlags::enableWindowingDynamicInitialBounds); } @Override - + public boolean enableWindowingEdgeDragResize() { return getValue(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE, - FeatureFlags::enableWindowingEdgeDragResize); + FeatureFlags::enableWindowingEdgeDragResize); } @Override - - public boolean enableWmExtensionsForAllFlag() { - return getValue(Flags.FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG, - FeatureFlags::enableWmExtensionsForAllFlag); + + public boolean enableWindowingScaledResizing() { + return getValue(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING, + FeatureFlags::enableWindowingScaledResizing); } @Override - + + public boolean enableWindowingTransitionHandlersObservers() { + return getValue(Flags.FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS, + FeatureFlags::enableWindowingTransitionHandlersObservers); + } + + @Override + public boolean enforceEdgeToEdge() { return getValue(Flags.FLAG_ENFORCE_EDGE_TO_EDGE, - FeatureFlags::enforceEdgeToEdge); + FeatureFlags::enforceEdgeToEdge); } @Override - + + public boolean ensureKeyguardDoesTransitionStarting() { + return getValue(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING, + FeatureFlags::ensureKeyguardDoesTransitionStarting); + } + + @Override + public boolean ensureWallpaperInTransitions() { return getValue(Flags.FLAG_ENSURE_WALLPAPER_IN_TRANSITIONS, - FeatureFlags::ensureWallpaperInTransitions); + FeatureFlags::ensureWallpaperInTransitions); } @Override - - public boolean explicitRefreshRateHints() { - return getValue(Flags.FLAG_EXPLICIT_REFRESH_RATE_HINTS, - FeatureFlags::explicitRefreshRateHints); + + public boolean ensureWallpaperInWearTransitions() { + return getValue(Flags.FLAG_ENSURE_WALLPAPER_IN_WEAR_TRANSITIONS, + FeatureFlags::ensureWallpaperInWearTransitions); } @Override - + + public boolean enterDesktopByDefaultOnFreeformDisplays() { + return getValue(Flags.FLAG_ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS, + FeatureFlags::enterDesktopByDefaultOnFreeformDisplays); + } + + @Override + + public boolean excludeCaptionFromAppBounds() { + return getValue(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS, + FeatureFlags::excludeCaptionFromAppBounds); + } + + @Override + + public boolean excludeDrawingAppThemeSnapshotFromLock() { + return getValue(Flags.FLAG_EXCLUDE_DRAWING_APP_THEME_SNAPSHOT_FROM_LOCK, + FeatureFlags::excludeDrawingAppThemeSnapshotFromLock); + } + + @Override + + public boolean excludeTaskFromRecents() { + return getValue(Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS, + FeatureFlags::excludeTaskFromRecents); + } + + @Override + public boolean fifoPriorityForMajorUiProcesses() { return getValue(Flags.FLAG_FIFO_PRIORITY_FOR_MAJOR_UI_PROCESSES, - FeatureFlags::fifoPriorityForMajorUiProcesses); + FeatureFlags::fifoPriorityForMajorUiProcesses); } @Override - - public boolean fixNoContainerUpdateWithoutResize() { - return getValue(Flags.FLAG_FIX_NO_CONTAINER_UPDATE_WITHOUT_RESIZE, - FeatureFlags::fixNoContainerUpdateWithoutResize); + + public boolean fixHideOverlayApi() { + return getValue(Flags.FLAG_FIX_HIDE_OVERLAY_API, + FeatureFlags::fixHideOverlayApi); } @Override - - public boolean fixPipRestoreToOverlay() { - return getValue(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY, - FeatureFlags::fixPipRestoreToOverlay); + + public boolean fixLayoutExistingTask() { + return getValue(Flags.FLAG_FIX_LAYOUT_EXISTING_TASK, + FeatureFlags::fixLayoutExistingTask); } @Override - - public boolean fullscreenDimFlag() { - return getValue(Flags.FLAG_FULLSCREEN_DIM_FLAG, - FeatureFlags::fullscreenDimFlag); + + public boolean fixViewRootCallTrace() { + return getValue(Flags.FLAG_FIX_VIEW_ROOT_CALL_TRACE, + FeatureFlags::fixViewRootCallTrace); } @Override - + + public boolean forceCloseTopTransparentFullscreenTask() { + return getValue(Flags.FLAG_FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK, + FeatureFlags::forceCloseTopTransparentFullscreenTask); + } + + @Override + + public boolean formFactorBasedDesktopFirstSwitch() { + return getValue(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH, + FeatureFlags::formFactorBasedDesktopFirstSwitch); + } + + @Override + public boolean getDimmerOnClosing() { return getValue(Flags.FLAG_GET_DIMMER_ON_CLOSING, - FeatureFlags::getDimmerOnClosing); + FeatureFlags::getDimmerOnClosing); } @Override - - public boolean immersiveAppRepositioning() { - return getValue(Flags.FLAG_IMMERSIVE_APP_REPOSITIONING, - FeatureFlags::immersiveAppRepositioning); + + public boolean ignoreAspectRatioRestrictionsForResizeableFreeformActivities() { + return getValue(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES, + FeatureFlags::ignoreAspectRatioRestrictionsForResizeableFreeformActivities); } @Override - - public boolean insetsControlChangedItem() { - return getValue(Flags.FLAG_INSETS_CONTROL_CHANGED_ITEM, - FeatureFlags::insetsControlChangedItem); + + public boolean ignoreCornerRadiusAndShadows() { + return getValue(Flags.FLAG_IGNORE_CORNER_RADIUS_AND_SHADOWS, + FeatureFlags::ignoreCornerRadiusAndShadows); } @Override - - public boolean insetsControlSeq() { - return getValue(Flags.FLAG_INSETS_CONTROL_SEQ, - FeatureFlags::insetsControlSeq); + + public boolean includeTopTransparentFullscreenTaskInDesktopHeuristic() { + return getValue(Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + FeatureFlags::includeTopTransparentFullscreenTaskInDesktopHeuristic); } @Override - + + public boolean inheritTaskBoundsForTrampolineTaskLaunches() { + return getValue(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES, + FeatureFlags::inheritTaskBoundsForTrampolineTaskLaunches); + } + + @Override + public boolean insetsDecoupledConfiguration() { return getValue(Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION, - FeatureFlags::insetsDecoupledConfiguration); + FeatureFlags::insetsDecoupledConfiguration); } @Override - - public boolean introduceSmootherDimmer() { - return getValue(Flags.FLAG_INTRODUCE_SMOOTHER_DIMMER, - FeatureFlags::introduceSmootherDimmer); + + public boolean jankApi() { + return getValue(Flags.FLAG_JANK_API, + FeatureFlags::jankApi); } @Override - - public boolean keyguardAppearTransition() { - return getValue(Flags.FLAG_KEYGUARD_APPEAR_TRANSITION, - FeatureFlags::keyguardAppearTransition); + + public boolean keepAppWindowHideWhileLocked() { + return getValue(Flags.FLAG_KEEP_APP_WINDOW_HIDE_WHILE_LOCKED, + FeatureFlags::keepAppWindowHideWhileLocked); } @Override - + + public boolean keyboardShortcutsToSwitchDesks() { + return getValue(Flags.FLAG_KEYBOARD_SHORTCUTS_TO_SWITCH_DESKS, + FeatureFlags::keyboardShortcutsToSwitchDesks); + } + + @Override + + public boolean keyguardGoingAwayTimeout() { + return getValue(Flags.FLAG_KEYGUARD_GOING_AWAY_TIMEOUT, + FeatureFlags::keyguardGoingAwayTimeout); + } + + @Override + public boolean letterboxBackgroundWallpaper() { return getValue(Flags.FLAG_LETTERBOX_BACKGROUND_WALLPAPER, - FeatureFlags::letterboxBackgroundWallpaper); + FeatureFlags::letterboxBackgroundWallpaper); } @Override - + public boolean movableCutoutConfiguration() { return getValue(Flags.FLAG_MOVABLE_CUTOUT_CONFIGURATION, - FeatureFlags::movableCutoutConfiguration); + FeatureFlags::movableCutoutConfiguration); } @Override - - public boolean moveAnimationOptionsToChange() { - return getValue(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE, - FeatureFlags::moveAnimationOptionsToChange); + + public boolean moveToExternalDisplayShortcut() { + return getValue(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + FeatureFlags::moveToExternalDisplayShortcut); } @Override - + public boolean multiCrop() { return getValue(Flags.FLAG_MULTI_CROP, - FeatureFlags::multiCrop); + FeatureFlags::multiCrop); } @Override - + public boolean navBarTransparentByDefault() { return getValue(Flags.FLAG_NAV_BAR_TRANSPARENT_BY_DEFAULT, - FeatureFlags::navBarTransparentByDefault); + FeatureFlags::navBarTransparentByDefault); } @Override - + + public boolean nestedTasksWithIndependentBounds() { + return getValue(Flags.FLAG_NESTED_TASKS_WITH_INDEPENDENT_BOUNDS, + FeatureFlags::nestedTasksWithIndependentBounds); + } + + @Override + public boolean noConsecutiveVisibilityEvents() { return getValue(Flags.FLAG_NO_CONSECUTIVE_VISIBILITY_EVENTS, - FeatureFlags::noConsecutiveVisibilityEvents); + FeatureFlags::noConsecutiveVisibilityEvents); } @Override - + + public boolean noDuplicateSurfaceDestroyedEvents() { + return getValue(Flags.FLAG_NO_DUPLICATE_SURFACE_DESTROYED_EVENTS, + FeatureFlags::noDuplicateSurfaceDestroyedEvents); + } + + @Override + public boolean noVisibilityEventOnDisplayStateChange() { return getValue(Flags.FLAG_NO_VISIBILITY_EVENT_ON_DISPLAY_STATE_CHANGE, - FeatureFlags::noVisibilityEventOnDisplayStateChange); + FeatureFlags::noVisibilityEventOnDisplayStateChange); } @Override - + public boolean offloadColorExtraction() { return getValue(Flags.FLAG_OFFLOAD_COLOR_EXTRACTION, - FeatureFlags::offloadColorExtraction); + FeatureFlags::offloadColorExtraction); } @Override - - public boolean predictiveBackSystemAnims() { - return getValue(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_ANIMS, - FeatureFlags::predictiveBackSystemAnims); + + public boolean portWindowSizeAnimation() { + return getValue(Flags.FLAG_PORT_WINDOW_SIZE_ANIMATION, + FeatureFlags::portWindowSizeAnimation); } @Override - + + public boolean predictiveBackDefaultEnableSdk36() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_DEFAULT_ENABLE_SDK_36, + FeatureFlags::predictiveBackDefaultEnableSdk36); + } + + @Override + + public boolean predictiveBackPrioritySystemNavigationObserver() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER, + FeatureFlags::predictiveBackPrioritySystemNavigationObserver); + } + + @Override + + public boolean predictiveBackSwipeEdgeNoneApi() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_SWIPE_EDGE_NONE_API, + FeatureFlags::predictiveBackSwipeEdgeNoneApi); + } + + @Override + + public boolean predictiveBackSystemOverrideCallback() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_OVERRIDE_CALLBACK, + FeatureFlags::predictiveBackSystemOverrideCallback); + } + + @Override + + public boolean predictiveBackThreeButtonNav() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV, + FeatureFlags::predictiveBackThreeButtonNav); + } + + @Override + + public boolean predictiveBackTimestampApi() { + return getValue(Flags.FLAG_PREDICTIVE_BACK_TIMESTAMP_API, + FeatureFlags::predictiveBackTimestampApi); + } + + @Override + + public boolean processPriorityPolicyForMultiWindowMode() { + return getValue(Flags.FLAG_PROCESS_PRIORITY_POLICY_FOR_MULTI_WINDOW_MODE, + FeatureFlags::processPriorityPolicyForMultiWindowMode); + } + + @Override + public boolean rearDisplayDisableForceDesktopSystemDecorations() { return getValue(Flags.FLAG_REAR_DISPLAY_DISABLE_FORCE_DESKTOP_SYSTEM_DECORATIONS, - FeatureFlags::rearDisplayDisableForceDesktopSystemDecorations); + FeatureFlags::rearDisplayDisableForceDesktopSystemDecorations); } @Override - + + public boolean recordTaskSnapshotsBeforeShutdown() { + return getValue(Flags.FLAG_RECORD_TASK_SNAPSHOTS_BEFORE_SHUTDOWN, + FeatureFlags::recordTaskSnapshotsBeforeShutdown); + } + + @Override + + public boolean reduceChangedExclusionRectsMsgs() { + return getValue(Flags.FLAG_REDUCE_CHANGED_EXCLUSION_RECTS_MSGS, + FeatureFlags::reduceChangedExclusionRectsMsgs); + } + + @Override + + public boolean reduceKeyguardTransitions() { + return getValue(Flags.FLAG_REDUCE_KEYGUARD_TRANSITIONS, + FeatureFlags::reduceKeyguardTransitions); + } + + @Override + + public boolean reduceTaskSnapshotMemoryUsage() { + return getValue(Flags.FLAG_REDUCE_TASK_SNAPSHOT_MEMORY_USAGE, + FeatureFlags::reduceTaskSnapshotMemoryUsage); + } + + @Override + + public boolean reduceUnnecessaryMeasure() { + return getValue(Flags.FLAG_REDUCE_UNNECESSARY_MEASURE, + FeatureFlags::reduceUnnecessaryMeasure); + } + + @Override + + public boolean relativeInsets() { + return getValue(Flags.FLAG_RELATIVE_INSETS, + FeatureFlags::relativeInsets); + } + + @Override + public boolean releaseSnapshotAggressively() { return getValue(Flags.FLAG_RELEASE_SNAPSHOT_AGGRESSIVELY, - FeatureFlags::releaseSnapshotAggressively); + FeatureFlags::releaseSnapshotAggressively); } @Override - - public boolean removePrepareSurfaceInPlacement() { - return getValue(Flags.FLAG_REMOVE_PREPARE_SURFACE_IN_PLACEMENT, - FeatureFlags::removePrepareSurfaceInPlacement); + + public boolean releaseUserAspectRatioWm() { + return getValue(Flags.FLAG_RELEASE_USER_ASPECT_RATIO_WM, + FeatureFlags::releaseUserAspectRatioWm); } @Override - + + public boolean removeActivityStarterDreamCallback() { + return getValue(Flags.FLAG_REMOVE_ACTIVITY_STARTER_DREAM_CALLBACK, + FeatureFlags::removeActivityStarterDreamCallback); + } + + @Override + + public boolean removeDeferHidingClient() { + return getValue(Flags.FLAG_REMOVE_DEFER_HIDING_CLIENT, + FeatureFlags::removeDeferHidingClient); + } + + @Override + + public boolean removeDepartTargetFromMotion() { + return getValue(Flags.FLAG_REMOVE_DEPART_TARGET_FROM_MOTION, + FeatureFlags::removeDepartTargetFromMotion); + } + + @Override + + public boolean reparentWindowTokenApi() { + return getValue(Flags.FLAG_REPARENT_WINDOW_TOKEN_API, + FeatureFlags::reparentWindowTokenApi); + } + + @Override + + public boolean respectNonTopVisibleFixedOrientation() { + return getValue(Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION, + FeatureFlags::respectNonTopVisibleFixedOrientation); + } + + @Override + + public boolean respectOrientationChangeForUnresizeable() { + return getValue(Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE, + FeatureFlags::respectOrientationChangeForUnresizeable); + } + + @Override + + public boolean safeRegionLetterboxing() { + return getValue(Flags.FLAG_SAFE_REGION_LETTERBOXING, + FeatureFlags::safeRegionLetterboxing); + } + + @Override + + public boolean safeReleaseSnapshotAggressively() { + return getValue(Flags.FLAG_SAFE_RELEASE_SNAPSHOT_AGGRESSIVELY, + FeatureFlags::safeReleaseSnapshotAggressively); + } + + @Override + + public boolean schedulingForNotificationShade() { + return getValue(Flags.FLAG_SCHEDULING_FOR_NOTIFICATION_SHADE, + FeatureFlags::schedulingForNotificationShade); + } + + @Override + + public boolean scrambleSnapshotFileName() { + return getValue(Flags.FLAG_SCRAMBLE_SNAPSHOT_FILE_NAME, + FeatureFlags::scrambleSnapshotFileName); + } + + @Override + public boolean screenRecordingCallbacks() { return getValue(Flags.FLAG_SCREEN_RECORDING_CALLBACKS, - FeatureFlags::screenRecordingCallbacks); + FeatureFlags::screenRecordingCallbacks); } @Override - + + public boolean scrollingFromLetterbox() { + return getValue(Flags.FLAG_SCROLLING_FROM_LETTERBOX, + FeatureFlags::scrollingFromLetterbox); + } + + @Override + public boolean sdkDesiredPresentTime() { return getValue(Flags.FLAG_SDK_DESIRED_PRESENT_TIME, - FeatureFlags::sdkDesiredPresentTime); + FeatureFlags::sdkDesiredPresentTime); } @Override - - public boolean secureWindowState() { - return getValue(Flags.FLAG_SECURE_WINDOW_STATE, - FeatureFlags::secureWindowState); - } - @Override - public boolean setScPropertiesInClient() { return getValue(Flags.FLAG_SET_SC_PROPERTIES_IN_CLIENT, - FeatureFlags::setScPropertiesInClient); + FeatureFlags::setScPropertiesInClient); } @Override - - public boolean skipSleepingWhenSwitchingDisplay() { - return getValue(Flags.FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY, - FeatureFlags::skipSleepingWhenSwitchingDisplay); + + public boolean showAppHandleLargeScreens() { + return getValue(Flags.FLAG_SHOW_APP_HANDLE_LARGE_SCREENS, + FeatureFlags::showAppHandleLargeScreens); } @Override - + + public boolean showDesktopExperienceDevOption() { + return getValue(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION, + FeatureFlags::showDesktopExperienceDevOption); + } + + @Override + + public boolean showDesktopWindowingDevOption() { + return getValue(Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FeatureFlags::showDesktopWindowingDevOption); + } + + @Override + + public boolean showHomeBehindDesktop() { + return getValue(Flags.FLAG_SHOW_HOME_BEHIND_DESKTOP, + FeatureFlags::showHomeBehindDesktop); + } + + @Override + + public boolean skipCompatUiEducationInDesktopMode() { + return getValue(Flags.FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE, + FeatureFlags::skipCompatUiEducationInDesktopMode); + } + + @Override + + public boolean skipDecorViewRelayoutWhenClosingBugfix() { + return getValue(Flags.FLAG_SKIP_DECOR_VIEW_RELAYOUT_WHEN_CLOSING_BUGFIX, + FeatureFlags::skipDecorViewRelayoutWhenClosingBugfix); + } + + @Override + + public boolean supportWidgetIntentsOnConnectedDisplay() { + return getValue(Flags.FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY, + FeatureFlags::supportWidgetIntentsOnConnectedDisplay); + } + + @Override + + public boolean supportsDragAssistantToMultiwindow() { + return getValue(Flags.FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW, + FeatureFlags::supportsDragAssistantToMultiwindow); + } + + @Override + public boolean supportsMultiInstanceSystemUi() { return getValue(Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, - FeatureFlags::supportsMultiInstanceSystemUi); + FeatureFlags::supportsMultiInstanceSystemUi); } @Override - + public boolean surfaceControlInputReceiver() { return getValue(Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER, - FeatureFlags::surfaceControlInputReceiver); + FeatureFlags::surfaceControlInputReceiver); } @Override - + public boolean surfaceTrustedOverlay() { return getValue(Flags.FLAG_SURFACE_TRUSTED_OVERLAY, - FeatureFlags::surfaceTrustedOverlay); + FeatureFlags::surfaceTrustedOverlay); } @Override - + public boolean syncScreenCapture() { return getValue(Flags.FLAG_SYNC_SCREEN_CAPTURE, - FeatureFlags::syncScreenCapture); + FeatureFlags::syncScreenCapture); } @Override - + + public boolean systemUiPostAnimationEnd() { + return getValue(Flags.FLAG_SYSTEM_UI_POST_ANIMATION_END, + FeatureFlags::systemUiPostAnimationEnd); + } + + @Override + public boolean taskFragmentSystemOrganizerFlag() { return getValue(Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG, - FeatureFlags::taskFragmentSystemOrganizerFlag); + FeatureFlags::taskFragmentSystemOrganizerFlag); } @Override - + + public boolean touchPassThroughOptIn() { + return getValue(Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN, + FeatureFlags::touchPassThroughOptIn); + } + + @Override + + public boolean trackSystemUiContextBeforeWms() { + return getValue(Flags.FLAG_TRACK_SYSTEM_UI_CONTEXT_BEFORE_WMS, + FeatureFlags::trackSystemUiContextBeforeWms); + } + + @Override + public boolean transitReadyTracking() { return getValue(Flags.FLAG_TRANSIT_READY_TRACKING, - FeatureFlags::transitReadyTracking); + FeatureFlags::transitReadyTracking); } @Override - + + public boolean transitTrackerPlumbing() { + return getValue(Flags.FLAG_TRANSIT_TRACKER_PLUMBING, + FeatureFlags::transitTrackerPlumbing); + } + + @Override + public boolean trustedPresentationListenerForWindow() { return getValue(Flags.FLAG_TRUSTED_PRESENTATION_LISTENER_FOR_WINDOW, - FeatureFlags::trustedPresentationListenerForWindow); + FeatureFlags::trustedPresentationListenerForWindow); } @Override - + + public boolean unifyBackNavigationTransition() { + return getValue(Flags.FLAG_UNIFY_BACK_NAVIGATION_TRANSITION, + FeatureFlags::unifyBackNavigationTransition); + } + + @Override + + public boolean universalResizableByDefault() { + return getValue(Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT, + FeatureFlags::universalResizableByDefault); + } + + @Override + public boolean untrustedEmbeddingAnyAppPermission() { return getValue(Flags.FLAG_UNTRUSTED_EMBEDDING_ANY_APP_PERMISSION, - FeatureFlags::untrustedEmbeddingAnyAppPermission); + FeatureFlags::untrustedEmbeddingAnyAppPermission); } @Override - + public boolean untrustedEmbeddingStateSharing() { return getValue(Flags.FLAG_UNTRUSTED_EMBEDDING_STATE_SHARING, - FeatureFlags::untrustedEmbeddingStateSharing); + FeatureFlags::untrustedEmbeddingStateSharing); } @Override - + + public boolean updateDimsWhenWindowShown() { + return getValue(Flags.FLAG_UPDATE_DIMS_WHEN_WINDOW_SHOWN, + FeatureFlags::updateDimsWhenWindowShown); + } + + @Override + + public boolean useCachedInsetsForDisplaySwitch() { + return getValue(Flags.FLAG_USE_CACHED_INSETS_FOR_DISPLAY_SWITCH, + FeatureFlags::useCachedInsetsForDisplaySwitch); + } + + @Override + + public boolean useRtFrameCallbackForSplashScreenTransfer() { + return getValue(Flags.FLAG_USE_RT_FRAME_CALLBACK_FOR_SPLASH_SCREEN_TRANSFER, + FeatureFlags::useRtFrameCallbackForSplashScreenTransfer); + } + + @Override + + public boolean useTasksDimOnly() { + return getValue(Flags.FLAG_USE_TASKS_DIM_ONLY, + FeatureFlags::useTasksDimOnly); + } + + @Override + + public boolean useVisibleRequestedForProcessTracker() { + return getValue(Flags.FLAG_USE_VISIBLE_REQUESTED_FOR_PROCESS_TRACKER, + FeatureFlags::useVisibleRequestedForProcessTracker); + } + + @Override + public boolean useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds() { return getValue(Flags.FLAG_USE_WINDOW_ORIGINAL_TOUCHABLE_REGION_WHEN_MAGNIFICATION_RECOMPUTE_BOUNDS, - FeatureFlags::useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds); + FeatureFlags::useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds); } @Override - - public boolean userMinAspectRatioAppDefault() { - return getValue(Flags.FLAG_USER_MIN_ASPECT_RATIO_APP_DEFAULT, - FeatureFlags::userMinAspectRatioAppDefault); + + public boolean vdmForceAppUniversalResizableApi() { + return getValue(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API, + FeatureFlags::vdmForceAppUniversalResizableApi); } @Override - - public boolean waitForTransitionOnDisplaySwitch() { - return getValue(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH, - FeatureFlags::waitForTransitionOnDisplaySwitch); - } - @Override - public boolean wallpaperOffsetAsync() { return getValue(Flags.FLAG_WALLPAPER_OFFSET_ASYNC, - FeatureFlags::wallpaperOffsetAsync); + FeatureFlags::wallpaperOffsetAsync); } @Override - - public boolean windowSessionRelayoutInfo() { - return getValue(Flags.FLAG_WINDOW_SESSION_RELAYOUT_INFO, - FeatureFlags::windowSessionRelayoutInfo); - } - @Override - - public boolean windowTokenConfigThreadSafe() { - return getValue(Flags.FLAG_WINDOW_TOKEN_CONFIG_THREAD_SAFE, - FeatureFlags::windowTokenConfigThreadSafe); + public boolean wlinfoOncreate() { + return getValue(Flags.FLAG_WLINFO_ONCREATE, + FeatureFlags::wlinfoOncreate); } public boolean isFlagReadOnlyOptimized(String flagName) { if (mReadOnlyFlagsSet.contains(flagName) && - isOptimizationEnabled()) { - return true; + isOptimizationEnabled()) { + return true; } return false; } - private boolean isOptimizationEnabled() { + + private boolean isOptimizationEnabled() { return false; } @@ -762,162 +1889,542 @@ public class CustomFeatureFlags implements FeatureFlags { public List getFlagNames() { return Arrays.asList( - Flags.FLAG_ACTIVITY_EMBEDDING_ANIMATION_CUSTOMIZATION_FLAG, - Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG, - Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG, - Flags.FLAG_ACTIVITY_SNAPSHOT_BY_DEFAULT, - Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG, - Flags.FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK, - Flags.FLAG_ALLOW_HIDE_SCM_BUTTON, - Flags.FLAG_ALLOWS_SCREEN_SIZE_DECOUPLED_FROM_STATUS_BAR_AND_CUTOUT, - Flags.FLAG_ALWAYS_DEFER_TRANSITION_WHEN_APPLY_WCT, - Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER, - Flags.FLAG_ALWAYS_UPDATE_WALLPAPER_PERMISSION, - Flags.FLAG_APP_COMPAT_PROPERTIES_API, - Flags.FLAG_APP_COMPAT_REFACTORING, - Flags.FLAG_BAL_DONT_BRING_EXISTING_BACKGROUND_TASK_STACK_TO_FG, - Flags.FLAG_BAL_IMPROVE_REAL_CALLER_VISIBILITY_CHECK, - Flags.FLAG_BAL_IMPROVED_METRICS, - Flags.FLAG_BAL_REQUIRE_OPT_IN_BY_PENDING_INTENT_CREATOR, - Flags.FLAG_BAL_REQUIRE_OPT_IN_SAME_UID, - Flags.FLAG_BAL_RESPECT_APP_SWITCH_STATE_WHEN_CHECK_BOUND_BY_FOREGROUND_UID, - Flags.FLAG_BAL_SHOW_TOASTS, - Flags.FLAG_BAL_SHOW_TOASTS_BLOCKED, - Flags.FLAG_BLAST_SYNC_NOTIFICATION_SHADE_ON_DISPLAY_SWITCH, - Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG, - Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM, - Flags.FLAG_CLOSE_TO_SQUARE_CONFIG_INCLUDES_STATUS_BAR, - Flags.FLAG_CONFIGURABLE_FONT_SCALE_DEFAULT, - Flags.FLAG_COVER_DISPLAY_OPT_IN, - Flags.FLAG_DEFER_DISPLAY_UPDATES, - Flags.FLAG_DELAY_NOTIFICATION_TO_MAGNIFICATION_WHEN_RECENTS_WINDOW_TO_FRONT_TRANSITION, - Flags.FLAG_DELEGATE_UNHANDLED_DRAGS, - Flags.FLAG_DELETE_CAPTURE_DISPLAY, - Flags.FLAG_DENSITY_390_API, - Flags.FLAG_DISABLE_OBJECT_POOL, - Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY, - Flags.FLAG_DO_NOT_CHECK_INTERSECTION_WHEN_NON_MAGNIFIABLE_WINDOW_TRANSITIONS, - Flags.FLAG_DRAW_SNAPSHOT_ASPECT_RATIO_MATCH, - Flags.FLAG_EDGE_TO_EDGE_BY_DEFAULT, - Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG, - Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR, - Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY, - Flags.FLAG_ENABLE_BUFFER_TRANSFORM_HINT_FROM_DISPLAY, - Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, - Flags.FLAG_ENABLE_COMPATUI_SYSUI_LAUNCHER, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASK_LIMIT, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_SCALED_RESIZING, - Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL, - Flags.FLAG_ENABLE_THEMED_APP_HEADERS, - Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, - Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE, - Flags.FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG, - Flags.FLAG_ENFORCE_EDGE_TO_EDGE, - Flags.FLAG_ENSURE_WALLPAPER_IN_TRANSITIONS, - Flags.FLAG_EXPLICIT_REFRESH_RATE_HINTS, - Flags.FLAG_FIFO_PRIORITY_FOR_MAJOR_UI_PROCESSES, - Flags.FLAG_FIX_NO_CONTAINER_UPDATE_WITHOUT_RESIZE, - Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY, - Flags.FLAG_FULLSCREEN_DIM_FLAG, - Flags.FLAG_GET_DIMMER_ON_CLOSING, - Flags.FLAG_IMMERSIVE_APP_REPOSITIONING, - Flags.FLAG_INSETS_CONTROL_CHANGED_ITEM, - Flags.FLAG_INSETS_CONTROL_SEQ, - Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION, - Flags.FLAG_INTRODUCE_SMOOTHER_DIMMER, - Flags.FLAG_KEYGUARD_APPEAR_TRANSITION, - Flags.FLAG_LETTERBOX_BACKGROUND_WALLPAPER, - Flags.FLAG_MOVABLE_CUTOUT_CONFIGURATION, - Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE, - Flags.FLAG_MULTI_CROP, - Flags.FLAG_NAV_BAR_TRANSPARENT_BY_DEFAULT, - Flags.FLAG_NO_CONSECUTIVE_VISIBILITY_EVENTS, - Flags.FLAG_NO_VISIBILITY_EVENT_ON_DISPLAY_STATE_CHANGE, - Flags.FLAG_OFFLOAD_COLOR_EXTRACTION, - Flags.FLAG_PREDICTIVE_BACK_SYSTEM_ANIMS, - Flags.FLAG_REAR_DISPLAY_DISABLE_FORCE_DESKTOP_SYSTEM_DECORATIONS, - Flags.FLAG_RELEASE_SNAPSHOT_AGGRESSIVELY, - Flags.FLAG_REMOVE_PREPARE_SURFACE_IN_PLACEMENT, - Flags.FLAG_SCREEN_RECORDING_CALLBACKS, - Flags.FLAG_SDK_DESIRED_PRESENT_TIME, - Flags.FLAG_SECURE_WINDOW_STATE, - Flags.FLAG_SET_SC_PROPERTIES_IN_CLIENT, - Flags.FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY, - Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, - Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER, - Flags.FLAG_SURFACE_TRUSTED_OVERLAY, - Flags.FLAG_SYNC_SCREEN_CAPTURE, - Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG, - Flags.FLAG_TRANSIT_READY_TRACKING, - Flags.FLAG_TRUSTED_PRESENTATION_LISTENER_FOR_WINDOW, - Flags.FLAG_UNTRUSTED_EMBEDDING_ANY_APP_PERMISSION, - Flags.FLAG_UNTRUSTED_EMBEDDING_STATE_SHARING, - Flags.FLAG_USE_WINDOW_ORIGINAL_TOUCHABLE_REGION_WHEN_MAGNIFICATION_RECOMPUTE_BOUNDS, - Flags.FLAG_USER_MIN_ASPECT_RATIO_APP_DEFAULT, - Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH, - Flags.FLAG_WALLPAPER_OFFSET_ASYNC, - Flags.FLAG_WINDOW_SESSION_RELAYOUT_INFO, - Flags.FLAG_WINDOW_TOKEN_CONFIG_THREAD_SAFE + Flags.FLAG_ACTION_MODE_EDGE_TO_EDGE, + Flags.FLAG_ACTIVITY_EMBEDDING_ANIMATION_CUSTOMIZATION_FLAG, + Flags.FLAG_ACTIVITY_EMBEDDING_DELAY_TASK_FRAGMENT_FINISH_FOR_ACTIVITY_LAUNCH, + Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG, + Flags.FLAG_ACTIVITY_EMBEDDING_METRICS, + Flags.FLAG_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK, + Flags.FLAG_ALLOW_HIDE_SCM_BUTTON, + Flags.FLAG_ALLOWS_SCREEN_SIZE_DECOUPLED_FROM_STATUS_BAR_AND_CUTOUT, + Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER, + Flags.FLAG_ALWAYS_UPDATE_WALLPAPER_PERMISSION, + Flags.FLAG_AOD_TRANSITION, + Flags.FLAG_APP_COMPAT_ASYNC_RELAYOUT, + Flags.FLAG_APP_COMPAT_PROPERTIES_API, + Flags.FLAG_APP_COMPAT_REFACTORING, + Flags.FLAG_APP_COMPAT_UI_FRAMEWORK, + Flags.FLAG_APP_HANDLE_NO_RELAYOUT_ON_EXCLUSION_CHANGE, + Flags.FLAG_APPLY_LIFECYCLE_ON_PIP_CHANGE, + Flags.FLAG_AVOID_REBINDING_INTENTIONALLY_DISCONNECTED_WALLPAPER, + Flags.FLAG_BACKUP_AND_RESTORE_FOR_USER_ASPECT_RATIO_SETTINGS, + Flags.FLAG_BAL_ADDITIONAL_LOGGING, + Flags.FLAG_BAL_ADDITIONAL_START_MODES, + Flags.FLAG_BAL_CLEAR_ALLOWLIST_DURATION, + Flags.FLAG_BAL_DONT_BRING_EXISTING_BACKGROUND_TASK_STACK_TO_FG, + Flags.FLAG_BAL_IMPROVE_REAL_CALLER_VISIBILITY_CHECK, + Flags.FLAG_BAL_IMPROVED_METRICS, + Flags.FLAG_BAL_REDUCE_GRACE_PERIOD, + Flags.FLAG_BAL_REQUIRE_OPT_IN_BY_PENDING_INTENT_CREATOR, + Flags.FLAG_BAL_RESPECT_APP_SWITCH_STATE_WHEN_CHECK_BOUND_BY_FOREGROUND_UID, + Flags.FLAG_BAL_SEND_INTENT_WITH_OPTIONS, + Flags.FLAG_BAL_SHOW_TOASTS_BLOCKED, + Flags.FLAG_BAL_STRICT_MODE_GRACE_PERIOD, + Flags.FLAG_BAL_STRICT_MODE_RO, + Flags.FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY, + Flags.FLAG_CACHE_WINDOW_STYLE, + Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM, + Flags.FLAG_CAMERA_COMPAT_FULLSCREEN_PICK_SAME_TASK_ACTIVITY, + Flags.FLAG_CHECK_DISABLED_SNAPSHOTS_IN_TASK_PERSISTER, + Flags.FLAG_CLEANUP_DISPATCH_PENDING_TRANSACTIONS_REMOTE_EXCEPTION, + Flags.FLAG_CLEAR_SYSTEM_VIBRATOR, + Flags.FLAG_CLOSE_TO_SQUARE_CONFIG_INCLUDES_STATUS_BAR, + Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE, + Flags.FLAG_CONFIGURABLE_FONT_SCALE_DEFAULT, + Flags.FLAG_COVER_DISPLAY_OPT_IN, + Flags.FLAG_DELAY_NOTIFICATION_TO_MAGNIFICATION_WHEN_RECENTS_WINDOW_TO_FRONT_TRANSITION, + Flags.FLAG_DELEGATE_BACK_GESTURE_TO_SHELL, + Flags.FLAG_DELEGATE_UNHANDLED_DRAGS, + Flags.FLAG_DELETE_CAPTURE_DISPLAY, + Flags.FLAG_DENSITY_390_API, + Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX, + Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, + Flags.FLAG_DISABLE_OPT_OUT_EDGE_TO_EDGE, + Flags.FLAG_DO_NOT_CHECK_INTERSECTION_WHEN_NON_MAGNIFIABLE_WINDOW_TRANSITIONS, + Flags.FLAG_EARLY_LAUNCH_HINT, + Flags.FLAG_EDGE_TO_EDGE_BY_DEFAULT, + Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS, + Flags.FLAG_ENABLE_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY, + Flags.FLAG_ENABLE_BORDER_SETTINGS, + Flags.FLAG_ENABLE_BUFFER_TRANSFORM_HINT_FROM_DISPLAY, + Flags.FLAG_ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT_API, + Flags.FLAG_ENABLE_CAMERA_COMPAT_TRACK_TASK_AND_APP_BUGFIX, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_CONVERSION, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS, + Flags.FLAG_ENABLE_CASCADING_WINDOWS, + Flags.FLAG_ENABLE_COMPAT_UI_VISIBILITY_STATUS, + Flags.FLAG_ENABLE_COMPATUI_SYSUI_LAUNCHER, + Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND, + Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP, + Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG, + Flags.FLAG_ENABLE_DESKTOP_APP_HANDLE_ANIMATION, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_CLOSE_TASK_ANIMATION_IN_DTC_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_IME_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION, + Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_SWIPE_BACK_MINIMIZE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_TASKBAR_ON_FREEFORM_DISPLAYS, + Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION_INTEGRATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_HSUM, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE_BUG_FIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASK_LIMIT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING, + Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR, + Flags.FLAG_ENABLE_DISPLAY_DISCONNECT_INTERACTION, + Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS, + Flags.FLAG_ENABLE_DISPLAY_RECONNECT_INTERACTION, + Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING, + Flags.FLAG_ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD, + Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DRAG_TO_MAXIMIZE, + Flags.FLAG_ENABLE_DYNAMIC_RADIUS_COMPUTATION_BUGFIX, + Flags.FLAG_ENABLE_FULL_SCREEN_WINDOW_ON_REMOVING_SPLIT_SCREEN_STAGE_BUGFIX, + Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, + Flags.FLAG_ENABLE_HANDLE_INPUT_FIX, + Flags.FLAG_ENABLE_HOLD_TO_DRAG_APP_HANDLE, + Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX, + Flags.FLAG_ENABLE_MINIMIZE_BUTTON, + Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION, + Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_MULTI_DISPLAY_SPLIT, + Flags.FLAG_ENABLE_MULTIDISPLAY_TRACKPAD_BACK_GESTURE, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND, + Flags.FLAG_ENABLE_NON_DEFAULT_DISPLAY_SPLIT, + Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS, + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF, + Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE, + Flags.FLAG_ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX, + Flags.FLAG_ENABLE_REQUEST_FULLSCREEN_BUGFIX, + Flags.FLAG_ENABLE_RESIZING_METRICS, + Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE, + Flags.FLAG_ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX, + Flags.FLAG_ENABLE_SIZE_COMPAT_MODE_IMPROVEMENTS_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX, + Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS, + Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL, + Flags.FLAG_ENABLE_TASKBAR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_TASKBAR_OVERFLOW, + Flags.FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION, + Flags.FLAG_ENABLE_THEMED_APP_HEADERS, + Flags.FLAG_ENABLE_TILE_RESIZING, + Flags.FLAG_ENABLE_TOP_VISIBLE_ROOT_TASK_PER_USER_TRACKING, + Flags.FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX, + Flags.FLAG_ENABLE_WINDOW_CONTEXT_RESOURCES_UPDATE_ON_CONFIG_CHANGE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, + Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE, + Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING, + Flags.FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS, + Flags.FLAG_ENFORCE_EDGE_TO_EDGE, + Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING, + Flags.FLAG_ENSURE_WALLPAPER_IN_TRANSITIONS, + Flags.FLAG_ENSURE_WALLPAPER_IN_WEAR_TRANSITIONS, + Flags.FLAG_ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS, + Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS, + Flags.FLAG_EXCLUDE_DRAWING_APP_THEME_SNAPSHOT_FROM_LOCK, + Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS, + Flags.FLAG_FIFO_PRIORITY_FOR_MAJOR_UI_PROCESSES, + Flags.FLAG_FIX_HIDE_OVERLAY_API, + Flags.FLAG_FIX_LAYOUT_EXISTING_TASK, + Flags.FLAG_FIX_VIEW_ROOT_CALL_TRACE, + Flags.FLAG_FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK, + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH, + Flags.FLAG_GET_DIMMER_ON_CLOSING, + Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES, + Flags.FLAG_IGNORE_CORNER_RADIUS_AND_SHADOWS, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES, + Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION, + Flags.FLAG_JANK_API, + Flags.FLAG_KEEP_APP_WINDOW_HIDE_WHILE_LOCKED, + Flags.FLAG_KEYBOARD_SHORTCUTS_TO_SWITCH_DESKS, + Flags.FLAG_KEYGUARD_GOING_AWAY_TIMEOUT, + Flags.FLAG_LETTERBOX_BACKGROUND_WALLPAPER, + Flags.FLAG_MOVABLE_CUTOUT_CONFIGURATION, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_MULTI_CROP, + Flags.FLAG_NAV_BAR_TRANSPARENT_BY_DEFAULT, + Flags.FLAG_NESTED_TASKS_WITH_INDEPENDENT_BOUNDS, + Flags.FLAG_NO_CONSECUTIVE_VISIBILITY_EVENTS, + Flags.FLAG_NO_DUPLICATE_SURFACE_DESTROYED_EVENTS, + Flags.FLAG_NO_VISIBILITY_EVENT_ON_DISPLAY_STATE_CHANGE, + Flags.FLAG_OFFLOAD_COLOR_EXTRACTION, + Flags.FLAG_PORT_WINDOW_SIZE_ANIMATION, + Flags.FLAG_PREDICTIVE_BACK_DEFAULT_ENABLE_SDK_36, + Flags.FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER, + Flags.FLAG_PREDICTIVE_BACK_SWIPE_EDGE_NONE_API, + Flags.FLAG_PREDICTIVE_BACK_SYSTEM_OVERRIDE_CALLBACK, + Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV, + Flags.FLAG_PREDICTIVE_BACK_TIMESTAMP_API, + Flags.FLAG_PROCESS_PRIORITY_POLICY_FOR_MULTI_WINDOW_MODE, + Flags.FLAG_REAR_DISPLAY_DISABLE_FORCE_DESKTOP_SYSTEM_DECORATIONS, + Flags.FLAG_RECORD_TASK_SNAPSHOTS_BEFORE_SHUTDOWN, + Flags.FLAG_REDUCE_CHANGED_EXCLUSION_RECTS_MSGS, + Flags.FLAG_REDUCE_KEYGUARD_TRANSITIONS, + Flags.FLAG_REDUCE_TASK_SNAPSHOT_MEMORY_USAGE, + Flags.FLAG_REDUCE_UNNECESSARY_MEASURE, + Flags.FLAG_RELATIVE_INSETS, + Flags.FLAG_RELEASE_SNAPSHOT_AGGRESSIVELY, + Flags.FLAG_RELEASE_USER_ASPECT_RATIO_WM, + Flags.FLAG_REMOVE_ACTIVITY_STARTER_DREAM_CALLBACK, + Flags.FLAG_REMOVE_DEFER_HIDING_CLIENT, + Flags.FLAG_REMOVE_DEPART_TARGET_FROM_MOTION, + Flags.FLAG_REPARENT_WINDOW_TOKEN_API, + Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION, + Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE, + Flags.FLAG_SAFE_REGION_LETTERBOXING, + Flags.FLAG_SAFE_RELEASE_SNAPSHOT_AGGRESSIVELY, + Flags.FLAG_SCHEDULING_FOR_NOTIFICATION_SHADE, + Flags.FLAG_SCRAMBLE_SNAPSHOT_FILE_NAME, + Flags.FLAG_SCREEN_RECORDING_CALLBACKS, + Flags.FLAG_SCROLLING_FROM_LETTERBOX, + Flags.FLAG_SDK_DESIRED_PRESENT_TIME, + Flags.FLAG_SET_SC_PROPERTIES_IN_CLIENT, + Flags.FLAG_SHOW_APP_HANDLE_LARGE_SCREENS, + Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION, + Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + Flags.FLAG_SHOW_HOME_BEHIND_DESKTOP, + Flags.FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE, + Flags.FLAG_SKIP_DECOR_VIEW_RELAYOUT_WHEN_CLOSING_BUGFIX, + Flags.FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY, + Flags.FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW, + Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, + Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER, + Flags.FLAG_SURFACE_TRUSTED_OVERLAY, + Flags.FLAG_SYNC_SCREEN_CAPTURE, + Flags.FLAG_SYSTEM_UI_POST_ANIMATION_END, + Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG, + Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN, + Flags.FLAG_TRACK_SYSTEM_UI_CONTEXT_BEFORE_WMS, + Flags.FLAG_TRANSIT_READY_TRACKING, + Flags.FLAG_TRANSIT_TRACKER_PLUMBING, + Flags.FLAG_TRUSTED_PRESENTATION_LISTENER_FOR_WINDOW, + Flags.FLAG_UNIFY_BACK_NAVIGATION_TRANSITION, + Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT, + Flags.FLAG_UNTRUSTED_EMBEDDING_ANY_APP_PERMISSION, + Flags.FLAG_UNTRUSTED_EMBEDDING_STATE_SHARING, + Flags.FLAG_UPDATE_DIMS_WHEN_WINDOW_SHOWN, + Flags.FLAG_USE_CACHED_INSETS_FOR_DISPLAY_SWITCH, + Flags.FLAG_USE_RT_FRAME_CALLBACK_FOR_SPLASH_SCREEN_TRANSFER, + Flags.FLAG_USE_TASKS_DIM_ONLY, + Flags.FLAG_USE_VISIBLE_REQUESTED_FOR_PROCESS_TRACKER, + Flags.FLAG_USE_WINDOW_ORIGINAL_TOUCHABLE_REGION_WHEN_MAGNIFICATION_RECOMPUTE_BOUNDS, + Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API, + Flags.FLAG_WALLPAPER_OFFSET_ASYNC, + Flags.FLAG_WLINFO_ONCREATE ); } private Set mReadOnlyFlagsSet = new HashSet<>( - Arrays.asList( - Flags.FLAG_ACTIVITY_SNAPSHOT_BY_DEFAULT, - Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG, - Flags.FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK, - Flags.FLAG_ALLOWS_SCREEN_SIZE_DECOUPLED_FROM_STATUS_BAR_AND_CUTOUT, - Flags.FLAG_APP_COMPAT_PROPERTIES_API, - Flags.FLAG_APP_COMPAT_REFACTORING, - Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG, - Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM, - Flags.FLAG_CONFIGURABLE_FONT_SCALE_DEFAULT, - Flags.FLAG_COVER_DISPLAY_OPT_IN, - Flags.FLAG_DEFER_DISPLAY_UPDATES, - Flags.FLAG_DELEGATE_UNHANDLED_DRAGS, - Flags.FLAG_DELETE_CAPTURE_DISPLAY, - Flags.FLAG_DENSITY_390_API, - Flags.FLAG_DISABLE_OBJECT_POOL, - Flags.FLAG_DRAW_SNAPSHOT_ASPECT_RATIO_MATCH, - Flags.FLAG_ENABLE_BUFFER_TRANSFORM_HINT_FROM_DISPLAY, - Flags.FLAG_ENABLE_SCALED_RESIZING, - Flags.FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG, - Flags.FLAG_ENFORCE_EDGE_TO_EDGE, - Flags.FLAG_EXPLICIT_REFRESH_RATE_HINTS, - Flags.FLAG_FIFO_PRIORITY_FOR_MAJOR_UI_PROCESSES, - Flags.FLAG_FIX_NO_CONTAINER_UPDATE_WITHOUT_RESIZE, - Flags.FLAG_GET_DIMMER_ON_CLOSING, - Flags.FLAG_INSETS_CONTROL_SEQ, - Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION, - Flags.FLAG_INTRODUCE_SMOOTHER_DIMMER, - Flags.FLAG_KEYGUARD_APPEAR_TRANSITION, - Flags.FLAG_MOVABLE_CUTOUT_CONFIGURATION, - Flags.FLAG_REAR_DISPLAY_DISABLE_FORCE_DESKTOP_SYSTEM_DECORATIONS, - Flags.FLAG_RELEASE_SNAPSHOT_AGGRESSIVELY, - Flags.FLAG_REMOVE_PREPARE_SURFACE_IN_PLACEMENT, - Flags.FLAG_SCREEN_RECORDING_CALLBACKS, - Flags.FLAG_SDK_DESIRED_PRESENT_TIME, - Flags.FLAG_SECURE_WINDOW_STATE, - Flags.FLAG_SET_SC_PROPERTIES_IN_CLIENT, - Flags.FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY, - Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, - Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER, - Flags.FLAG_SURFACE_TRUSTED_OVERLAY, - Flags.FLAG_SYNC_SCREEN_CAPTURE, - Flags.FLAG_TRUSTED_PRESENTATION_LISTENER_FOR_WINDOW, - Flags.FLAG_UNTRUSTED_EMBEDDING_ANY_APP_PERMISSION, - Flags.FLAG_UNTRUSTED_EMBEDDING_STATE_SHARING, - Flags.FLAG_WALLPAPER_OFFSET_ASYNC, - Flags.FLAG_WINDOW_SESSION_RELAYOUT_INFO, - "" - ) + Arrays.asList( + Flags.FLAG_ACTION_MODE_EDGE_TO_EDGE, + Flags.FLAG_ACTIVITY_EMBEDDING_ANIMATION_CUSTOMIZATION_FLAG, + Flags.FLAG_ACTIVITY_EMBEDDING_DELAY_TASK_FRAGMENT_FINISH_FOR_ACTIVITY_LAUNCH, + Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG, + Flags.FLAG_ACTIVITY_EMBEDDING_METRICS, + Flags.FLAG_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK, + Flags.FLAG_ALLOW_HIDE_SCM_BUTTON, + Flags.FLAG_ALLOWS_SCREEN_SIZE_DECOUPLED_FROM_STATUS_BAR_AND_CUTOUT, + Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER, + Flags.FLAG_ALWAYS_UPDATE_WALLPAPER_PERMISSION, + Flags.FLAG_AOD_TRANSITION, + Flags.FLAG_APP_COMPAT_ASYNC_RELAYOUT, + Flags.FLAG_APP_COMPAT_PROPERTIES_API, + Flags.FLAG_APP_COMPAT_REFACTORING, + Flags.FLAG_APP_COMPAT_UI_FRAMEWORK, + Flags.FLAG_APP_HANDLE_NO_RELAYOUT_ON_EXCLUSION_CHANGE, + Flags.FLAG_APPLY_LIFECYCLE_ON_PIP_CHANGE, + Flags.FLAG_AVOID_REBINDING_INTENTIONALLY_DISCONNECTED_WALLPAPER, + Flags.FLAG_BACKUP_AND_RESTORE_FOR_USER_ASPECT_RATIO_SETTINGS, + Flags.FLAG_BAL_ADDITIONAL_LOGGING, + Flags.FLAG_BAL_ADDITIONAL_START_MODES, + Flags.FLAG_BAL_CLEAR_ALLOWLIST_DURATION, + Flags.FLAG_BAL_DONT_BRING_EXISTING_BACKGROUND_TASK_STACK_TO_FG, + Flags.FLAG_BAL_IMPROVE_REAL_CALLER_VISIBILITY_CHECK, + Flags.FLAG_BAL_IMPROVED_METRICS, + Flags.FLAG_BAL_REDUCE_GRACE_PERIOD, + Flags.FLAG_BAL_REQUIRE_OPT_IN_BY_PENDING_INTENT_CREATOR, + Flags.FLAG_BAL_RESPECT_APP_SWITCH_STATE_WHEN_CHECK_BOUND_BY_FOREGROUND_UID, + Flags.FLAG_BAL_SEND_INTENT_WITH_OPTIONS, + Flags.FLAG_BAL_SHOW_TOASTS_BLOCKED, + Flags.FLAG_BAL_STRICT_MODE_GRACE_PERIOD, + Flags.FLAG_BAL_STRICT_MODE_RO, + Flags.FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY, + Flags.FLAG_CACHE_WINDOW_STYLE, + Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM, + Flags.FLAG_CAMERA_COMPAT_FULLSCREEN_PICK_SAME_TASK_ACTIVITY, + Flags.FLAG_CHECK_DISABLED_SNAPSHOTS_IN_TASK_PERSISTER, + Flags.FLAG_CLEANUP_DISPATCH_PENDING_TRANSACTIONS_REMOTE_EXCEPTION, + Flags.FLAG_CLEAR_SYSTEM_VIBRATOR, + Flags.FLAG_CLOSE_TO_SQUARE_CONFIG_INCLUDES_STATUS_BAR, + Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE, + Flags.FLAG_CONFIGURABLE_FONT_SCALE_DEFAULT, + Flags.FLAG_COVER_DISPLAY_OPT_IN, + Flags.FLAG_DELAY_NOTIFICATION_TO_MAGNIFICATION_WHEN_RECENTS_WINDOW_TO_FRONT_TRANSITION, + Flags.FLAG_DELEGATE_BACK_GESTURE_TO_SHELL, + Flags.FLAG_DELEGATE_UNHANDLED_DRAGS, + Flags.FLAG_DELETE_CAPTURE_DISPLAY, + Flags.FLAG_DENSITY_390_API, + Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX, + Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, + Flags.FLAG_DISABLE_OPT_OUT_EDGE_TO_EDGE, + Flags.FLAG_DO_NOT_CHECK_INTERSECTION_WHEN_NON_MAGNIFIABLE_WINDOW_TRANSITIONS, + Flags.FLAG_EARLY_LAUNCH_HINT, + Flags.FLAG_EDGE_TO_EDGE_BY_DEFAULT, + Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS, + Flags.FLAG_ENABLE_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY, + Flags.FLAG_ENABLE_BORDER_SETTINGS, + Flags.FLAG_ENABLE_BUFFER_TRANSFORM_HINT_FROM_DISPLAY, + Flags.FLAG_ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT, + Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT_API, + Flags.FLAG_ENABLE_CAMERA_COMPAT_TRACK_TASK_AND_APP_BUGFIX, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_CONVERSION, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS, + Flags.FLAG_ENABLE_CASCADING_WINDOWS, + Flags.FLAG_ENABLE_COMPAT_UI_VISIBILITY_STATUS, + Flags.FLAG_ENABLE_COMPATUI_SYSUI_LAUNCHER, + Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND, + Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP, + Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG, + Flags.FLAG_ENABLE_DESKTOP_APP_HANDLE_ANIMATION, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_CLOSE_TASK_ANIMATION_IN_DTC_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_IME_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION, + Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_SWIPE_BACK_MINIMIZE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_TASKBAR_ON_FREEFORM_DISPLAYS, + Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION_INTEGRATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_HSUM, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE_BUG_FIX, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASK_LIMIT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING, + Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR, + Flags.FLAG_ENABLE_DISPLAY_DISCONNECT_INTERACTION, + Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS, + Flags.FLAG_ENABLE_DISPLAY_RECONNECT_INTERACTION, + Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING, + Flags.FLAG_ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD, + Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DRAG_TO_MAXIMIZE, + Flags.FLAG_ENABLE_DYNAMIC_RADIUS_COMPUTATION_BUGFIX, + Flags.FLAG_ENABLE_FULL_SCREEN_WINDOW_ON_REMOVING_SPLIT_SCREEN_STAGE_BUGFIX, + Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, + Flags.FLAG_ENABLE_HANDLE_INPUT_FIX, + Flags.FLAG_ENABLE_HOLD_TO_DRAG_APP_HANDLE, + Flags.FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX, + Flags.FLAG_ENABLE_MINIMIZE_BUTTON, + Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION, + Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_MULTI_DISPLAY_SPLIT, + Flags.FLAG_ENABLE_MULTIDISPLAY_TRACKPAD_BACK_GESTURE, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND, + Flags.FLAG_ENABLE_NON_DEFAULT_DISPLAY_SPLIT, + Flags.FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS, + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF, + Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE, + Flags.FLAG_ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX, + Flags.FLAG_ENABLE_REQUEST_FULLSCREEN_BUGFIX, + Flags.FLAG_ENABLE_RESIZING_METRICS, + Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE, + Flags.FLAG_ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX, + Flags.FLAG_ENABLE_SIZE_COMPAT_MODE_IMPROVEMENTS_FOR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX, + Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS, + Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL, + Flags.FLAG_ENABLE_TASKBAR_CONNECTED_DISPLAYS, + Flags.FLAG_ENABLE_TASKBAR_OVERFLOW, + Flags.FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION, + Flags.FLAG_ENABLE_THEMED_APP_HEADERS, + Flags.FLAG_ENABLE_TILE_RESIZING, + Flags.FLAG_ENABLE_TOP_VISIBLE_ROOT_TASK_PER_USER_TRACKING, + Flags.FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX, + Flags.FLAG_ENABLE_WINDOW_CONTEXT_RESOURCES_UPDATE_ON_CONFIG_CHANGE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, + Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE, + Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING, + Flags.FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS, + Flags.FLAG_ENFORCE_EDGE_TO_EDGE, + Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING, + Flags.FLAG_ENSURE_WALLPAPER_IN_TRANSITIONS, + Flags.FLAG_ENSURE_WALLPAPER_IN_WEAR_TRANSITIONS, + Flags.FLAG_ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS, + Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS, + Flags.FLAG_EXCLUDE_DRAWING_APP_THEME_SNAPSHOT_FROM_LOCK, + Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS, + Flags.FLAG_FIFO_PRIORITY_FOR_MAJOR_UI_PROCESSES, + Flags.FLAG_FIX_HIDE_OVERLAY_API, + Flags.FLAG_FIX_LAYOUT_EXISTING_TASK, + Flags.FLAG_FIX_VIEW_ROOT_CALL_TRACE, + Flags.FLAG_FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK, + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH, + Flags.FLAG_GET_DIMMER_ON_CLOSING, + Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES, + Flags.FLAG_IGNORE_CORNER_RADIUS_AND_SHADOWS, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES, + Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION, + Flags.FLAG_JANK_API, + Flags.FLAG_KEEP_APP_WINDOW_HIDE_WHILE_LOCKED, + Flags.FLAG_KEYBOARD_SHORTCUTS_TO_SWITCH_DESKS, + Flags.FLAG_KEYGUARD_GOING_AWAY_TIMEOUT, + Flags.FLAG_LETTERBOX_BACKGROUND_WALLPAPER, + Flags.FLAG_MOVABLE_CUTOUT_CONFIGURATION, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_MULTI_CROP, + Flags.FLAG_NAV_BAR_TRANSPARENT_BY_DEFAULT, + Flags.FLAG_NESTED_TASKS_WITH_INDEPENDENT_BOUNDS, + Flags.FLAG_NO_CONSECUTIVE_VISIBILITY_EVENTS, + Flags.FLAG_NO_DUPLICATE_SURFACE_DESTROYED_EVENTS, + Flags.FLAG_NO_VISIBILITY_EVENT_ON_DISPLAY_STATE_CHANGE, + Flags.FLAG_OFFLOAD_COLOR_EXTRACTION, + Flags.FLAG_PORT_WINDOW_SIZE_ANIMATION, + Flags.FLAG_PREDICTIVE_BACK_DEFAULT_ENABLE_SDK_36, + Flags.FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER, + Flags.FLAG_PREDICTIVE_BACK_SWIPE_EDGE_NONE_API, + Flags.FLAG_PREDICTIVE_BACK_SYSTEM_OVERRIDE_CALLBACK, + Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV, + Flags.FLAG_PREDICTIVE_BACK_TIMESTAMP_API, + Flags.FLAG_PROCESS_PRIORITY_POLICY_FOR_MULTI_WINDOW_MODE, + Flags.FLAG_REAR_DISPLAY_DISABLE_FORCE_DESKTOP_SYSTEM_DECORATIONS, + Flags.FLAG_RECORD_TASK_SNAPSHOTS_BEFORE_SHUTDOWN, + Flags.FLAG_REDUCE_CHANGED_EXCLUSION_RECTS_MSGS, + Flags.FLAG_REDUCE_KEYGUARD_TRANSITIONS, + Flags.FLAG_REDUCE_TASK_SNAPSHOT_MEMORY_USAGE, + Flags.FLAG_REDUCE_UNNECESSARY_MEASURE, + Flags.FLAG_RELATIVE_INSETS, + Flags.FLAG_RELEASE_SNAPSHOT_AGGRESSIVELY, + Flags.FLAG_RELEASE_USER_ASPECT_RATIO_WM, + Flags.FLAG_REMOVE_ACTIVITY_STARTER_DREAM_CALLBACK, + Flags.FLAG_REMOVE_DEFER_HIDING_CLIENT, + Flags.FLAG_REMOVE_DEPART_TARGET_FROM_MOTION, + Flags.FLAG_REPARENT_WINDOW_TOKEN_API, + Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION, + Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE, + Flags.FLAG_SAFE_REGION_LETTERBOXING, + Flags.FLAG_SAFE_RELEASE_SNAPSHOT_AGGRESSIVELY, + Flags.FLAG_SCHEDULING_FOR_NOTIFICATION_SHADE, + Flags.FLAG_SCRAMBLE_SNAPSHOT_FILE_NAME, + Flags.FLAG_SCREEN_RECORDING_CALLBACKS, + Flags.FLAG_SCROLLING_FROM_LETTERBOX, + Flags.FLAG_SDK_DESIRED_PRESENT_TIME, + Flags.FLAG_SET_SC_PROPERTIES_IN_CLIENT, + Flags.FLAG_SHOW_APP_HANDLE_LARGE_SCREENS, + Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION, + Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + Flags.FLAG_SHOW_HOME_BEHIND_DESKTOP, + Flags.FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE, + Flags.FLAG_SKIP_DECOR_VIEW_RELAYOUT_WHEN_CLOSING_BUGFIX, + Flags.FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY, + Flags.FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW, + Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, + Flags.FLAG_SURFACE_CONTROL_INPUT_RECEIVER, + Flags.FLAG_SURFACE_TRUSTED_OVERLAY, + Flags.FLAG_SYNC_SCREEN_CAPTURE, + Flags.FLAG_SYSTEM_UI_POST_ANIMATION_END, + Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG, + Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN, + Flags.FLAG_TRACK_SYSTEM_UI_CONTEXT_BEFORE_WMS, + Flags.FLAG_TRANSIT_READY_TRACKING, + Flags.FLAG_TRANSIT_TRACKER_PLUMBING, + Flags.FLAG_TRUSTED_PRESENTATION_LISTENER_FOR_WINDOW, + Flags.FLAG_UNIFY_BACK_NAVIGATION_TRANSITION, + Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT, + Flags.FLAG_UNTRUSTED_EMBEDDING_ANY_APP_PERMISSION, + Flags.FLAG_UNTRUSTED_EMBEDDING_STATE_SHARING, + Flags.FLAG_UPDATE_DIMS_WHEN_WINDOW_SHOWN, + Flags.FLAG_USE_CACHED_INSETS_FOR_DISPLAY_SWITCH, + Flags.FLAG_USE_RT_FRAME_CALLBACK_FOR_SPLASH_SCREEN_TRANSFER, + Flags.FLAG_USE_TASKS_DIM_ONLY, + Flags.FLAG_USE_VISIBLE_REQUESTED_FOR_PROCESS_TRACKER, + Flags.FLAG_USE_WINDOW_ORIGINAL_TOUCHABLE_REGION_WHEN_MAGNIFICATION_RECOMPUTE_BOUNDS, + Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API, + Flags.FLAG_WALLPAPER_OFFSET_ASYNC, + Flags.FLAG_WLINFO_ONCREATE, + "" + ) ); } diff --git a/flags/src/com/android/window/flags2/FakeFeatureFlagsImpl.java b/flags/src/com/android/window/flags2/FakeFeatureFlagsImpl.java index 3fd351436e..222ea4b0ec 100644 --- a/flags/src/com/android/window/flags2/FakeFeatureFlagsImpl.java +++ b/flags/src/com/android/window/flags2/FakeFeatureFlagsImpl.java @@ -3,7 +3,6 @@ package com.android.window.flags2; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; - /** @hide */ public class FakeFeatureFlagsImpl extends CustomFeatureFlags { private final Map mFlagMap = new HashMap<>(); diff --git a/flags/src/com/android/window/flags2/FeatureFlags.java b/flags/src/com/android/window/flags2/FeatureFlags.java index bba4aabe12..d8c9510e77 100644 --- a/flags/src/com/android/window/flags2/FeatureFlags.java +++ b/flags/src/com/android/window/flags2/FeatureFlags.java @@ -3,212 +3,1064 @@ package com.android.window.flags2; /** @hide */ public interface FeatureFlags { - + + + + boolean actionModeEdgeToEdge(); + + + boolean activityEmbeddingAnimationCustomizationFlag(); - + + + + boolean activityEmbeddingDelayTaskFragmentFinishForActivityLaunch(); + + + boolean activityEmbeddingInteractiveDividerFlag(); - - boolean activityEmbeddingOverlayPresentationFlag(); - - boolean activitySnapshotByDefault(); - - boolean activityWindowInfoFlag(); - + + + + boolean activityEmbeddingMetrics(); + + + + boolean activityEmbeddingSupportForConnectedDisplays(); + + + boolean allowDisableActivityRecordInputSink(); - + + + boolean allowHideScmButton(); - + + + boolean allowsScreenSizeDecoupledFromStatusBarAndCutout(); - - boolean alwaysDeferTransitionWhenApplyWct(); - + + + boolean alwaysDrawMagnificationFullscreenBorder(); - + + + boolean alwaysUpdateWallpaperPermission(); - + + + + boolean aodTransition(); + + + + boolean appCompatAsyncRelayout(); + + + boolean appCompatPropertiesApi(); - + + + boolean appCompatRefactoring(); - + + + + boolean appCompatUiFramework(); + + + + boolean appHandleNoRelayoutOnExclusionChange(); + + + + boolean applyLifecycleOnPipChange(); + + + + boolean avoidRebindingIntentionallyDisconnectedWallpaper(); + + + + boolean backupAndRestoreForUserAspectRatioSettings(); + + + + boolean balAdditionalLogging(); + + + + boolean balAdditionalStartModes(); + + + + boolean balClearAllowlistDuration(); + + + boolean balDontBringExistingBackgroundTaskStackToFg(); - + + + boolean balImproveRealCallerVisibilityCheck(); - + + + boolean balImprovedMetrics(); - + + + + boolean balReduceGracePeriod(); + + + boolean balRequireOptInByPendingIntentCreator(); - - boolean balRequireOptInSameUid(); - + + + boolean balRespectAppSwitchStateWhenCheckBoundByForegroundUid(); - - boolean balShowToasts(); - + + + + boolean balSendIntentWithOptions(); + + + boolean balShowToastsBlocked(); - - boolean blastSyncNotificationShadeOnDisplaySwitch(); - - boolean bundleClientTransactionFlag(); - + + + + boolean balStrictModeGracePeriod(); + + + + boolean balStrictModeRo(); + + + + boolean betterSupportNonMatchParentActivity(); + + + + boolean cacheWindowStyle(); + + + boolean cameraCompatForFreeform(); - + + + + boolean cameraCompatFullscreenPickSameTaskActivity(); + + + + boolean checkDisabledSnapshotsInTaskPersister(); + + + + boolean cleanupDispatchPendingTransactionsRemoteException(); + + + + boolean clearSystemVibrator(); + + + boolean closeToSquareConfigIncludesStatusBar(); - + + + + boolean condenseConfigurationChangeForSimpleMode(); + + + boolean configurableFontScaleDefault(); - + + + boolean coverDisplayOptIn(); - - boolean deferDisplayUpdates(); - + + + boolean delayNotificationToMagnificationWhenRecentsWindowToFrontTransition(); - + + + + boolean delegateBackGestureToShell(); + + + boolean delegateUnhandledDrags(); - + + + boolean deleteCaptureDisplay(); - + + + boolean density390Api(); - - boolean disableObjectPool(); - - boolean disableThinLetterboxingPolicy(); - + + + + boolean disableDesktopLaunchParamsOutsideDesktopBugFix(); + + + + boolean disableNonResizableAppSnapResizing(); + + + + boolean disableOptOutEdgeToEdge(); + + + boolean doNotCheckIntersectionWhenNonMagnifiableWindowTransitions(); - - boolean drawSnapshotAspectRatioMatch(); - + + + + boolean earlyLaunchHint(); + + + boolean edgeToEdgeByDefault(); - - boolean embeddedActivityBackNavFlag(); - - boolean enableAdditionalWindowsAboveStatusBar(); - + + + + boolean enableAccessibleCustomHeaders(); + + + + boolean enableActivityEmbeddingSupportForConnectedDisplays(); + + + boolean enableAppHeaderWithTaskDensity(); - + + + + boolean enableBorderSettings(); + + + boolean enableBufferTransformHintFromDisplay(); - + + + + boolean enableBugFixesForSecondaryDisplay(); + + + boolean enableCameraCompatForDesktopWindowing(); - + + + + boolean enableCameraCompatForDesktopWindowingOptOut(); + + + + boolean enableCameraCompatForDesktopWindowingOptOutApi(); + + + + boolean enableCameraCompatTrackTaskAndAppBugfix(); + + + + boolean enableCaptionCompatInsetConversion(); + + + + boolean enableCaptionCompatInsetForceConsumption(); + + + + boolean enableCaptionCompatInsetForceConsumptionAlways(); + + + + boolean enableCascadingWindows(); + + + + boolean enableCompatUiVisibilityStatus(); + + + boolean enableCompatuiSysuiLauncher(); - + + + + boolean enableConnectedDisplaysDnd(); + + + + boolean enableConnectedDisplaysPip(); + + + + boolean enableConnectedDisplaysWindowDrag(); + + + + boolean enableDesktopAppHandleAnimation(); + + + + boolean enableDesktopAppLaunchAlttabTransitions(); + + + + boolean enableDesktopAppLaunchAlttabTransitionsBugfix(); + + + + boolean enableDesktopAppLaunchTransitions(); + + + + boolean enableDesktopAppLaunchTransitionsBugfix(); + + + + boolean enableDesktopCloseShortcutBugfix(); + + + + boolean enableDesktopCloseTaskAnimationInDtcBugfix(); + + + + boolean enableDesktopImeBugfix(); + + + + boolean enableDesktopImmersiveDragBugfix(); + + + + boolean enableDesktopIndicatorInSeparateThreadBugfix(); + + + + boolean enableDesktopModeThroughDevOption(); + + + + boolean enableDesktopOpeningDeeplinkMinimizeAnimationBugfix(); + + + + boolean enableDesktopRecentsTransitionsCornersBugfix(); + + + + boolean enableDesktopSwipeBackMinimizeAnimationBugfix(); + + + + boolean enableDesktopSystemDialogsTransitions(); + + + + boolean enableDesktopTabTearingMinimizeAnimationBugfix(); + + + + boolean enableDesktopTaskbarOnFreeformDisplays(); + + + + boolean enableDesktopTrampolineCloseAnimationBugfix(); + + + + boolean enableDesktopWallpaperActivityForSystemUser(); + + + + boolean enableDesktopWindowingAppHandleEducation(); + + + + boolean enableDesktopWindowingAppToWeb(); + + + + boolean enableDesktopWindowingAppToWebEducation(); + + + + boolean enableDesktopWindowingAppToWebEducationIntegration(); + + + + boolean enableDesktopWindowingBackNavigation(); + + + + boolean enableDesktopWindowingEnterTransitionBugfix(); + + + + boolean enableDesktopWindowingEnterTransitions(); + + + + boolean enableDesktopWindowingExitByMinimizeTransitionBugfix(); + + + + boolean enableDesktopWindowingExitTransitions(); + + + + boolean enableDesktopWindowingExitTransitionsBugfix(); + + + + boolean enableDesktopWindowingHsum(); + + + boolean enableDesktopWindowingImmersiveHandleHiding(); - + + + boolean enableDesktopWindowingModalsPolicy(); - + + + boolean enableDesktopWindowingMode(); - + + + + boolean enableDesktopWindowingMultiInstanceFeatures(); + + + + boolean enableDesktopWindowingPersistence(); + + + + boolean enableDesktopWindowingPip(); + + + boolean enableDesktopWindowingQuickSwitch(); - - boolean enableDesktopWindowingScvhCache(); - + + + + boolean enableDesktopWindowingScvhCacheBugFix(); + + + boolean enableDesktopWindowingSizeConstraints(); - + + + boolean enableDesktopWindowingTaskLimit(); - + + + boolean enableDesktopWindowingTaskbarRunningApps(); - + + + + boolean enableDesktopWindowingTransitions(); + + + boolean enableDesktopWindowingWallpaperActivity(); - - boolean enableScaledResizing(); - + + + + boolean enableDeviceStateAutoRotateSettingLogging(); + + + + boolean enableDeviceStateAutoRotateSettingRefactor(); + + + + boolean enableDisplayDisconnectInteraction(); + + + + boolean enableDisplayFocusInShellTransitions(); + + + + boolean enableDisplayReconnectInteraction(); + + + + boolean enableDisplayWindowingModeSwitching(); + + + + boolean enableDragResizeSetUpInBgThread(); + + + + boolean enableDragToDesktopIncomingTransitionsBugfix(); + + + + boolean enableDragToMaximize(); + + + + boolean enableDynamicRadiusComputationBugfix(); + + + + boolean enableFullScreenWindowOnRemovingSplitScreenStageBugfix(); + + + + boolean enableFullyImmersiveInDesktop(); + + + + boolean enableHandleInputFix(); + + + + boolean enableHoldToDragAppHandle(); + + + + boolean enableInputLayerTransitionFix(); + + + + boolean enableMinimizeButton(); + + + + boolean enableModalsFullscreenWithPermission(); + + + + boolean enableMoveToNextDisplayShortcut(); + + + + boolean enableMultiDisplaySplit(); + + + + boolean enableMultidisplayTrackpadBackGesture(); + + + + boolean enableMultipleDesktopsBackend(); + + + + boolean enableMultipleDesktopsFrontend(); + + + + boolean enableNonDefaultDisplaySplit(); + + + + boolean enableOpaqueBackgroundForTransparentWindows(); + + + + boolean enablePerDisplayDesktopWallpaperActivity(); + + + + boolean enablePerDisplayPackageContextCacheInStatusbarNotif(); + + + + boolean enablePersistingDisplaySizeForConnectedDisplays(); + + + + boolean enablePresentationForConnectedDisplays(); + + + + boolean enableProjectedDisplayDesktopMode(); + + + + boolean enableQuickswitchDesktopSplitBugfix(); + + + + boolean enableRequestFullscreenBugfix(); + + + + boolean enableResizingMetrics(); + + + + boolean enableRestartMenuForConnectedDisplays(); + + + + boolean enableRestoreToPreviousSizeFromDesktopImmersive(); + + + + boolean enableShellInitialBoundsRegressionBugFix(); + + + + boolean enableSizeCompatModeImprovementsForConnectedDisplays(); + + + + boolean enableStartLaunchTransitionFromTaskbarBugfix(); + + + + boolean enableTaskResizingKeyboardShortcuts(); + + + boolean enableTaskStackObserverInShell(); - + + + + boolean enableTaskbarConnectedDisplays(); + + + + boolean enableTaskbarOverflow(); + + + + boolean enableTaskbarRecentsLayoutTransition(); + + + boolean enableThemedAppHeaders(); - + + + + boolean enableTileResizing(); + + + + boolean enableTopVisibleRootTaskPerUserTracking(); + + + + boolean enableVisualIndicatorInTransitionBugfix(); + + + + boolean enableWindowContextResourcesUpdateOnConfigChange(); + + + boolean enableWindowingDynamicInitialBounds(); - + + + boolean enableWindowingEdgeDragResize(); - - boolean enableWmExtensionsForAllFlag(); - + + + + boolean enableWindowingScaledResizing(); + + + + boolean enableWindowingTransitionHandlersObservers(); + + + boolean enforceEdgeToEdge(); - + + + + boolean ensureKeyguardDoesTransitionStarting(); + + + boolean ensureWallpaperInTransitions(); - - boolean explicitRefreshRateHints(); - + + + + boolean ensureWallpaperInWearTransitions(); + + + + boolean enterDesktopByDefaultOnFreeformDisplays(); + + + + boolean excludeCaptionFromAppBounds(); + + + + boolean excludeDrawingAppThemeSnapshotFromLock(); + + + + boolean excludeTaskFromRecents(); + + + boolean fifoPriorityForMajorUiProcesses(); - - boolean fixNoContainerUpdateWithoutResize(); - - boolean fixPipRestoreToOverlay(); - - boolean fullscreenDimFlag(); - + + + + boolean fixHideOverlayApi(); + + + + boolean fixLayoutExistingTask(); + + + + boolean fixViewRootCallTrace(); + + + + boolean forceCloseTopTransparentFullscreenTask(); + + + + boolean formFactorBasedDesktopFirstSwitch(); + + + boolean getDimmerOnClosing(); - - boolean immersiveAppRepositioning(); - - boolean insetsControlChangedItem(); - - boolean insetsControlSeq(); - + + + + boolean ignoreAspectRatioRestrictionsForResizeableFreeformActivities(); + + + + boolean ignoreCornerRadiusAndShadows(); + + + + boolean includeTopTransparentFullscreenTaskInDesktopHeuristic(); + + + + boolean inheritTaskBoundsForTrampolineTaskLaunches(); + + + boolean insetsDecoupledConfiguration(); - - boolean introduceSmootherDimmer(); - - boolean keyguardAppearTransition(); - + + + + boolean jankApi(); + + + + boolean keepAppWindowHideWhileLocked(); + + + + boolean keyboardShortcutsToSwitchDesks(); + + + + boolean keyguardGoingAwayTimeout(); + + + boolean letterboxBackgroundWallpaper(); - + + + boolean movableCutoutConfiguration(); - - boolean moveAnimationOptionsToChange(); - + + + + boolean moveToExternalDisplayShortcut(); + + + boolean multiCrop(); - + + + boolean navBarTransparentByDefault(); - + + + + boolean nestedTasksWithIndependentBounds(); + + + boolean noConsecutiveVisibilityEvents(); - + + + + boolean noDuplicateSurfaceDestroyedEvents(); + + + boolean noVisibilityEventOnDisplayStateChange(); - + + + boolean offloadColorExtraction(); - - boolean predictiveBackSystemAnims(); - + + + + boolean portWindowSizeAnimation(); + + + + boolean predictiveBackDefaultEnableSdk36(); + + + + boolean predictiveBackPrioritySystemNavigationObserver(); + + + + boolean predictiveBackSwipeEdgeNoneApi(); + + + + boolean predictiveBackSystemOverrideCallback(); + + + + boolean predictiveBackThreeButtonNav(); + + + + boolean predictiveBackTimestampApi(); + + + + boolean processPriorityPolicyForMultiWindowMode(); + + + boolean rearDisplayDisableForceDesktopSystemDecorations(); - + + + + boolean recordTaskSnapshotsBeforeShutdown(); + + + + boolean reduceChangedExclusionRectsMsgs(); + + + + boolean reduceKeyguardTransitions(); + + + + boolean reduceTaskSnapshotMemoryUsage(); + + + + boolean reduceUnnecessaryMeasure(); + + + + boolean relativeInsets(); + + + boolean releaseSnapshotAggressively(); - - boolean removePrepareSurfaceInPlacement(); - + + + + boolean releaseUserAspectRatioWm(); + + + + boolean removeActivityStarterDreamCallback(); + + + + boolean removeDeferHidingClient(); + + + + boolean removeDepartTargetFromMotion(); + + + + boolean reparentWindowTokenApi(); + + + + boolean respectNonTopVisibleFixedOrientation(); + + + + boolean respectOrientationChangeForUnresizeable(); + + + + boolean safeRegionLetterboxing(); + + + + boolean safeReleaseSnapshotAggressively(); + + + + boolean schedulingForNotificationShade(); + + + + boolean scrambleSnapshotFileName(); + + + boolean screenRecordingCallbacks(); - + + + + boolean scrollingFromLetterbox(); + + + boolean sdkDesiredPresentTime(); - - boolean secureWindowState(); - + + + boolean setScPropertiesInClient(); - - boolean skipSleepingWhenSwitchingDisplay(); - + + + + boolean showAppHandleLargeScreens(); + + + + boolean showDesktopExperienceDevOption(); + + + + boolean showDesktopWindowingDevOption(); + + + + boolean showHomeBehindDesktop(); + + + + boolean skipCompatUiEducationInDesktopMode(); + + + + boolean skipDecorViewRelayoutWhenClosingBugfix(); + + + + boolean supportWidgetIntentsOnConnectedDisplay(); + + + + boolean supportsDragAssistantToMultiwindow(); + + + boolean supportsMultiInstanceSystemUi(); - + + + boolean surfaceControlInputReceiver(); - + + + boolean surfaceTrustedOverlay(); - + + + boolean syncScreenCapture(); - + + + + boolean systemUiPostAnimationEnd(); + + + boolean taskFragmentSystemOrganizerFlag(); - + + + + boolean touchPassThroughOptIn(); + + + + boolean trackSystemUiContextBeforeWms(); + + + boolean transitReadyTracking(); - + + + + boolean transitTrackerPlumbing(); + + + boolean trustedPresentationListenerForWindow(); - + + + + boolean unifyBackNavigationTransition(); + + + + boolean universalResizableByDefault(); + + + boolean untrustedEmbeddingAnyAppPermission(); - + + + boolean untrustedEmbeddingStateSharing(); - + + + + boolean updateDimsWhenWindowShown(); + + + + boolean useCachedInsetsForDisplaySwitch(); + + + + boolean useRtFrameCallbackForSplashScreenTransfer(); + + + + boolean useTasksDimOnly(); + + + + boolean useVisibleRequestedForProcessTracker(); + + + boolean useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds(); - - boolean userMinAspectRatioAppDefault(); - - boolean waitForTransitionOnDisplaySwitch(); - + + + + boolean vdmForceAppUniversalResizableApi(); + + + boolean wallpaperOffsetAsync(); - - boolean windowSessionRelayoutInfo(); - - boolean windowTokenConfigThreadSafe(); + + + + boolean wlinfoOncreate(); } diff --git a/flags/src/com/android/window/flags2/FeatureFlagsImpl.java b/flags/src/com/android/window/flags2/FeatureFlagsImpl.java index 56df1ad9a1..142a639298 100644 --- a/flags/src/com/android/window/flags2/FeatureFlagsImpl.java +++ b/flags/src/com/android/window/flags2/FeatureFlagsImpl.java @@ -1,1638 +1,1860 @@ package com.android.window.flags2; // TODO(b/303773055): Remove the annotation after access issue is resolved. - -import com.android.quickstep.util.DeviceConfigHelper; - -import java.nio.file.Files; -import java.nio.file.Paths; /** @hide */ public final class FeatureFlagsImpl implements FeatureFlags { - private static final boolean isReadFromNew = Files.exists(Paths.get("/metadata/aconfig/boot/enable_only_new_storage")); - private static volatile boolean isCached = false; - private static volatile boolean accessibility_is_cached = false; - private static volatile boolean large_screen_experiences_app_compat_is_cached = false; - private static volatile boolean lse_desktop_experience_is_cached = false; - private static volatile boolean multitasking_is_cached = false; - private static volatile boolean responsible_apis_is_cached = false; - private static volatile boolean systemui_is_cached = false; - private static volatile boolean wear_frameworks_is_cached = false; - private static volatile boolean window_surfaces_is_cached = false; - private static volatile boolean windowing_frontend_is_cached = false; - private static volatile boolean windowing_sdk_is_cached = false; - private static boolean activityEmbeddingAnimationCustomizationFlag = false; - private static boolean activityEmbeddingInteractiveDividerFlag = true; - private static boolean activityEmbeddingOverlayPresentationFlag = true; - private static boolean allowHideScmButton = true; - private static boolean alwaysDeferTransitionWhenApplyWct = true; - private static boolean alwaysDrawMagnificationFullscreenBorder = true; - private static boolean alwaysUpdateWallpaperPermission = true; - private static boolean balDontBringExistingBackgroundTaskStackToFg = true; - private static boolean balImproveRealCallerVisibilityCheck = true; - private static boolean balImprovedMetrics = true; - private static boolean balRequireOptInByPendingIntentCreator = true; - private static boolean balRequireOptInSameUid = false; - private static boolean balRespectAppSwitchStateWhenCheckBoundByForegroundUid = true; - private static boolean balShowToasts = false; - private static boolean balShowToastsBlocked = true; - private static boolean blastSyncNotificationShadeOnDisplaySwitch = true; - private static boolean closeToSquareConfigIncludesStatusBar = false; - private static boolean delayNotificationToMagnificationWhenRecentsWindowToFrontTransition = true; - private static boolean disableThinLetterboxingPolicy = true; - private static boolean doNotCheckIntersectionWhenNonMagnifiableWindowTransitions = true; - private static boolean edgeToEdgeByDefault = false; - private static boolean embeddedActivityBackNavFlag = true; - private static boolean enableAdditionalWindowsAboveStatusBar = false; - private static boolean enableAppHeaderWithTaskDensity = true; - private static boolean enableCameraCompatForDesktopWindowing = true; - private static boolean enableCompatuiSysuiLauncher = false; - private static boolean enableDesktopWindowingImmersiveHandleHiding = false; - private static boolean enableDesktopWindowingModalsPolicy = true; - private static boolean enableDesktopWindowingMode = false; - private static boolean enableDesktopWindowingQuickSwitch = false; - private static boolean enableDesktopWindowingScvhCache = false; - private static boolean enableDesktopWindowingSizeConstraints = false; - private static boolean enableDesktopWindowingTaskLimit = true; - private static boolean enableDesktopWindowingTaskbarRunningApps = true; - private static boolean enableDesktopWindowingWallpaperActivity = false; - private static boolean enableTaskStackObserverInShell = true; - private static boolean enableThemedAppHeaders = true; - private static boolean enableWindowingDynamicInitialBounds = false; - private static boolean enableWindowingEdgeDragResize = false; - private static boolean ensureWallpaperInTransitions = false; - private static boolean fixPipRestoreToOverlay = true; - private static boolean fullscreenDimFlag = true; - private static boolean immersiveAppRepositioning = true; - private static boolean insetsControlChangedItem = false; - private static boolean letterboxBackgroundWallpaper = false; - private static boolean moveAnimationOptionsToChange = true; - private static boolean multiCrop = true; - private static boolean navBarTransparentByDefault = true; - private static boolean noConsecutiveVisibilityEvents = true; - private static boolean noVisibilityEventOnDisplayStateChange = true; - private static boolean offloadColorExtraction = false; - private static boolean predictiveBackSystemAnims = true; - private static boolean taskFragmentSystemOrganizerFlag = true; - private static boolean transitReadyTracking = false; - private static boolean useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds = true; - private static boolean userMinAspectRatioAppDefault = true; - private static boolean waitForTransitionOnDisplaySwitch = true; - private static boolean windowTokenConfigThreadSafe = true; + @Override - private void init() { - isCached = true; - } - - - - - private void load_overrides_accessibility() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - alwaysDrawMagnificationFullscreenBorder = - properties.getBoolean(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER, true); - delayNotificationToMagnificationWhenRecentsWindowToFrontTransition = - properties.getBoolean(Flags.FLAG_DELAY_NOTIFICATION_TO_MAGNIFICATION_WHEN_RECENTS_WINDOW_TO_FRONT_TRANSITION, true); - doNotCheckIntersectionWhenNonMagnifiableWindowTransitions = - properties.getBoolean(Flags.FLAG_DO_NOT_CHECK_INTERSECTION_WHEN_NON_MAGNIFIABLE_WINDOW_TRANSITIONS, true); - useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds = - properties.getBoolean(Flags.FLAG_USE_WINDOW_ORIGINAL_TOUCHABLE_REGION_WHEN_MAGNIFICATION_RECOMPUTE_BOUNDS, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace accessibility " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - accessibility_is_cached = true; - } - - private void load_overrides_large_screen_experiences_app_compat() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - allowHideScmButton = - properties.getBoolean(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON, true); - disableThinLetterboxingPolicy = - properties.getBoolean(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY, true); - enableCompatuiSysuiLauncher = - properties.getBoolean(Flags.FLAG_ENABLE_COMPATUI_SYSUI_LAUNCHER, false); - immersiveAppRepositioning = - properties.getBoolean(Flags.FLAG_IMMERSIVE_APP_REPOSITIONING, true); - letterboxBackgroundWallpaper = - properties.getBoolean(Flags.FLAG_LETTERBOX_BACKGROUND_WALLPAPER, false); - userMinAspectRatioAppDefault = - properties.getBoolean(Flags.FLAG_USER_MIN_ASPECT_RATIO_APP_DEFAULT, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace large_screen_experiences_app_compat " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - large_screen_experiences_app_compat_is_cached = true; - } - - private void load_overrides_lse_desktop_experience() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - enableAdditionalWindowsAboveStatusBar = - properties.getBoolean(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR, false); - enableAppHeaderWithTaskDensity = - properties.getBoolean(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY, true); - enableCameraCompatForDesktopWindowing = - properties.getBoolean(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, true); - enableDesktopWindowingImmersiveHandleHiding = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING, false); - enableDesktopWindowingModalsPolicy = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, true); - enableDesktopWindowingMode = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, true); - enableDesktopWindowingQuickSwitch = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH, false); - enableDesktopWindowingScvhCache = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE, false); - enableDesktopWindowingSizeConstraints = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS, false); - enableDesktopWindowingTaskLimit = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASK_LIMIT, true); - enableDesktopWindowingTaskbarRunningApps = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, true); - enableDesktopWindowingWallpaperActivity = - properties.getBoolean(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, false); - enableTaskStackObserverInShell = - properties.getBoolean(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL, true); - enableThemedAppHeaders = - properties.getBoolean(Flags.FLAG_ENABLE_THEMED_APP_HEADERS, true); - enableWindowingDynamicInitialBounds = - properties.getBoolean(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, false); - enableWindowingEdgeDragResize = - properties.getBoolean(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE, false); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace lse_desktop_experience " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - lse_desktop_experience_is_cached = true; - } - - private void load_overrides_multitasking() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace multitasking " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - multitasking_is_cached = true; - } - - private void load_overrides_responsible_apis() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - balDontBringExistingBackgroundTaskStackToFg = - properties.getBoolean(Flags.FLAG_BAL_DONT_BRING_EXISTING_BACKGROUND_TASK_STACK_TO_FG, true); - balImproveRealCallerVisibilityCheck = - properties.getBoolean(Flags.FLAG_BAL_IMPROVE_REAL_CALLER_VISIBILITY_CHECK, true); - balImprovedMetrics = - properties.getBoolean(Flags.FLAG_BAL_IMPROVED_METRICS, true); - balRequireOptInByPendingIntentCreator = - properties.getBoolean(Flags.FLAG_BAL_REQUIRE_OPT_IN_BY_PENDING_INTENT_CREATOR, true); - balRequireOptInSameUid = - properties.getBoolean(Flags.FLAG_BAL_REQUIRE_OPT_IN_SAME_UID, false); - balRespectAppSwitchStateWhenCheckBoundByForegroundUid = - properties.getBoolean(Flags.FLAG_BAL_RESPECT_APP_SWITCH_STATE_WHEN_CHECK_BOUND_BY_FOREGROUND_UID, true); - balShowToasts = - properties.getBoolean(Flags.FLAG_BAL_SHOW_TOASTS, false); - balShowToastsBlocked = - properties.getBoolean(Flags.FLAG_BAL_SHOW_TOASTS_BLOCKED, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace responsible_apis " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - responsible_apis_is_cached = true; - } - - private void load_overrides_systemui() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - multiCrop = - properties.getBoolean(Flags.FLAG_MULTI_CROP, true); - noConsecutiveVisibilityEvents = - properties.getBoolean(Flags.FLAG_NO_CONSECUTIVE_VISIBILITY_EVENTS, true); - offloadColorExtraction = - properties.getBoolean(Flags.FLAG_OFFLOAD_COLOR_EXTRACTION, false); - predictiveBackSystemAnims = - properties.getBoolean(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_ANIMS, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace systemui " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - systemui_is_cached = true; - } - - private void load_overrides_wear_frameworks() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - alwaysUpdateWallpaperPermission = - properties.getBoolean(Flags.FLAG_ALWAYS_UPDATE_WALLPAPER_PERMISSION, true); - noVisibilityEventOnDisplayStateChange = - properties.getBoolean(Flags.FLAG_NO_VISIBILITY_EVENT_ON_DISPLAY_STATE_CHANGE, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace wear_frameworks " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - wear_frameworks_is_cached = true; - } - - private void load_overrides_window_surfaces() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace window_surfaces " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - window_surfaces_is_cached = true; - } - - private void load_overrides_windowing_frontend() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - blastSyncNotificationShadeOnDisplaySwitch = - properties.getBoolean(Flags.FLAG_BLAST_SYNC_NOTIFICATION_SHADE_ON_DISPLAY_SWITCH, true); - closeToSquareConfigIncludesStatusBar = - properties.getBoolean(Flags.FLAG_CLOSE_TO_SQUARE_CONFIG_INCLUDES_STATUS_BAR, false); - edgeToEdgeByDefault = - properties.getBoolean(Flags.FLAG_EDGE_TO_EDGE_BY_DEFAULT, false); - ensureWallpaperInTransitions = - properties.getBoolean(Flags.FLAG_ENSURE_WALLPAPER_IN_TRANSITIONS, false); - navBarTransparentByDefault = - properties.getBoolean(Flags.FLAG_NAV_BAR_TRANSPARENT_BY_DEFAULT, true); - transitReadyTracking = - properties.getBoolean(Flags.FLAG_TRANSIT_READY_TRACKING, false); - waitForTransitionOnDisplaySwitch = - properties.getBoolean(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace windowing_frontend " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - windowing_frontend_is_cached = true; - } - - private void load_overrides_windowing_sdk() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - activityEmbeddingAnimationCustomizationFlag = - properties.getBoolean(Flags.FLAG_ACTIVITY_EMBEDDING_ANIMATION_CUSTOMIZATION_FLAG, false); - activityEmbeddingInteractiveDividerFlag = - properties.getBoolean(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG, true); - activityEmbeddingOverlayPresentationFlag = - properties.getBoolean(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG, true); - alwaysDeferTransitionWhenApplyWct = - properties.getBoolean(Flags.FLAG_ALWAYS_DEFER_TRANSITION_WHEN_APPLY_WCT, true); - embeddedActivityBackNavFlag = - properties.getBoolean(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG, true); - fixPipRestoreToOverlay = - properties.getBoolean(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY, true); - fullscreenDimFlag = - properties.getBoolean(Flags.FLAG_FULLSCREEN_DIM_FLAG, true); - insetsControlChangedItem = - properties.getBoolean(Flags.FLAG_INSETS_CONTROL_CHANGED_ITEM, false); - moveAnimationOptionsToChange = - properties.getBoolean(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE, true); - taskFragmentSystemOrganizerFlag = - properties.getBoolean(Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG, true); - windowTokenConfigThreadSafe = - properties.getBoolean(Flags.FLAG_WINDOW_TOKEN_CONFIG_THREAD_SAFE, true); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace windowing_sdk " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } catch (SecurityException e) { - // for isolated process case, skip loading flag value from the storage, use the default - } - windowing_sdk_is_cached = true; + public boolean actionModeEdgeToEdge() { + return false; } @Override - + + public boolean activityEmbeddingAnimationCustomizationFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return activityEmbeddingAnimationCustomizationFlag; - + return true; } @Override - + + + public boolean activityEmbeddingDelayTaskFragmentFinishForActivityLaunch() { + return false; + } + + @Override + + public boolean activityEmbeddingInteractiveDividerFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return activityEmbeddingInteractiveDividerFlag; - - } - - @Override - - public boolean activityEmbeddingOverlayPresentationFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return activityEmbeddingOverlayPresentationFlag; - - } - - @Override - - public boolean activitySnapshotByDefault() { return true; - } @Override - - public boolean activityWindowInfoFlag() { - return true; + + public boolean activityEmbeddingMetrics() { + return false; } @Override - + + + public boolean activityEmbeddingSupportForConnectedDisplays() { + return false; + } + + @Override + + public boolean allowDisableActivityRecordInputSink() { return true; - } @Override - + + public boolean allowHideScmButton() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!large_screen_experiences_app_compat_is_cached) { - load_overrides_large_screen_experiences_app_compat(); - } - } - return allowHideScmButton; - + return true; } @Override - + + public boolean allowsScreenSizeDecoupledFromStatusBarAndCutout() { return true; - } @Override - - public boolean alwaysDeferTransitionWhenApplyWct() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return alwaysDeferTransitionWhenApplyWct; - } - @Override - public boolean alwaysDrawMagnificationFullscreenBorder() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return alwaysDrawMagnificationFullscreenBorder; - + return true; } @Override - + + public boolean alwaysUpdateWallpaperPermission() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!wear_frameworks_is_cached) { - load_overrides_wear_frameworks(); - } - } - return alwaysUpdateWallpaperPermission; - + return true; } @Override - + + + public boolean aodTransition() { + return false; + } + + @Override + + + public boolean appCompatAsyncRelayout() { + return false; + } + + @Override + + public boolean appCompatPropertiesApi() { return true; - } @Override - + + public boolean appCompatRefactoring() { return false; - } @Override - + + + public boolean appCompatUiFramework() { + return false; + } + + @Override + + + public boolean appHandleNoRelayoutOnExclusionChange() { + return false; + } + + @Override + + + public boolean applyLifecycleOnPipChange() { + return false; + } + + @Override + + + public boolean avoidRebindingIntentionallyDisconnectedWallpaper() { + return true; + } + + @Override + + + public boolean backupAndRestoreForUserAspectRatioSettings() { + return false; + } + + @Override + + + public boolean balAdditionalLogging() { + return false; + } + + @Override + + + public boolean balAdditionalStartModes() { + return true; + } + + @Override + + + public boolean balClearAllowlistDuration() { + return false; + } + + @Override + + public boolean balDontBringExistingBackgroundTaskStackToFg() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balDontBringExistingBackgroundTaskStackToFg; - + return true; } @Override - + + public boolean balImproveRealCallerVisibilityCheck() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balImproveRealCallerVisibilityCheck; - + return true; } @Override - + + public boolean balImprovedMetrics() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balImprovedMetrics; - + return true; } @Override - + + + public boolean balReduceGracePeriod() { + return false; + } + + @Override + + public boolean balRequireOptInByPendingIntentCreator() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balRequireOptInByPendingIntentCreator; - + return true; } @Override - - public boolean balRequireOptInSameUid() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balRequireOptInSameUid; - } - @Override - public boolean balRespectAppSwitchStateWhenCheckBoundByForegroundUid() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balRespectAppSwitchStateWhenCheckBoundByForegroundUid; - + return true; } @Override - - public boolean balShowToasts() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balShowToasts; + + public boolean balSendIntentWithOptions() { + return true; } @Override - + + public boolean balShowToastsBlocked() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!responsible_apis_is_cached) { - load_overrides_responsible_apis(); - } - } - return balShowToastsBlocked; - + return false; } @Override - - public boolean blastSyncNotificationShadeOnDisplaySwitch() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_frontend_is_cached) { - load_overrides_windowing_frontend(); - } - } - return blastSyncNotificationShadeOnDisplaySwitch; - } - @Override - - public boolean bundleClientTransactionFlag() { + public boolean balStrictModeGracePeriod() { return true; - } @Override - + + + public boolean balStrictModeRo() { + return true; + } + + @Override + + + public boolean betterSupportNonMatchParentActivity() { + return true; + } + + @Override + + + public boolean cacheWindowStyle() { + return true; + } + + @Override + + public boolean cameraCompatForFreeform() { + return false; + } + + @Override + + + public boolean cameraCompatFullscreenPickSameTaskActivity() { + return false; + } + + @Override + + + public boolean checkDisabledSnapshotsInTaskPersister() { return true; - } @Override - + + + public boolean cleanupDispatchPendingTransactionsRemoteException() { + return false; + } + + @Override + + + public boolean clearSystemVibrator() { + return true; + } + + @Override + + public boolean closeToSquareConfigIncludesStatusBar() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_frontend_is_cached) { - load_overrides_windowing_frontend(); - } - } - return closeToSquareConfigIncludesStatusBar; - + return false; } @Override - + + + public boolean condenseConfigurationChangeForSimpleMode() { + return true; + } + + @Override + + public boolean configurableFontScaleDefault() { return true; - } @Override - + + public boolean coverDisplayOptIn() { return true; - } @Override - - public boolean deferDisplayUpdates() { - return true; - } - @Override - public boolean delayNotificationToMagnificationWhenRecentsWindowToFrontTransition() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return delayNotificationToMagnificationWhenRecentsWindowToFrontTransition; - + return true; } @Override - + + + public boolean delegateBackGestureToShell() { + return false; + } + + @Override + + public boolean delegateUnhandledDrags() { return true; - } @Override - + + public boolean deleteCaptureDisplay() { return true; - } @Override - + + public boolean density390Api() { return true; - } @Override - - public boolean disableObjectPool() { + + + public boolean disableDesktopLaunchParamsOutsideDesktopBugFix() { return true; - } @Override - - public boolean disableThinLetterboxingPolicy() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!large_screen_experiences_app_compat_is_cached) { - load_overrides_large_screen_experiences_app_compat(); - } - } - return disableThinLetterboxingPolicy; + + public boolean disableNonResizableAppSnapResizing() { + return true; } @Override - + + + public boolean disableOptOutEdgeToEdge() { + return true; + } + + @Override + + public boolean doNotCheckIntersectionWhenNonMagnifiableWindowTransitions() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return doNotCheckIntersectionWhenNonMagnifiableWindowTransitions; - - } - - @Override - - public boolean drawSnapshotAspectRatioMatch() { return false; - } @Override - + + + public boolean earlyLaunchHint() { + return true; + } + + @Override + + public boolean edgeToEdgeByDefault() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_frontend_is_cached) { - load_overrides_windowing_frontend(); - } - } - return edgeToEdgeByDefault; - + return false; } @Override - - public boolean embeddedActivityBackNavFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return embeddedActivityBackNavFlag; + + public boolean enableAccessibleCustomHeaders() { + return true; } @Override - - public boolean enableAdditionalWindowsAboveStatusBar() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableAdditionalWindowsAboveStatusBar; + + public boolean enableActivityEmbeddingSupportForConnectedDisplays() { + return false; } @Override - + + public boolean enableAppHeaderWithTaskDensity() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableAppHeaderWithTaskDensity; - + return true; } @Override - + + + public boolean enableBorderSettings() { + return false; + } + + @Override + + public boolean enableBufferTransformHintFromDisplay() { return true; - } @Override - - public boolean enableCameraCompatForDesktopWindowing() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableCameraCompatForDesktopWindowing; - } - @Override - - public boolean enableCompatuiSysuiLauncher() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!large_screen_experiences_app_compat_is_cached) { - load_overrides_large_screen_experiences_app_compat(); - } - } - return enableCompatuiSysuiLauncher; - - } - - @Override - - public boolean enableDesktopWindowingImmersiveHandleHiding() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingImmersiveHandleHiding; - - } - - @Override - - public boolean enableDesktopWindowingModalsPolicy() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingModalsPolicy; - - } - - @Override - - public boolean enableDesktopWindowingMode() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingMode; - - } - - @Override - - public boolean enableDesktopWindowingQuickSwitch() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingQuickSwitch; - - } - - @Override - - public boolean enableDesktopWindowingScvhCache() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingScvhCache; - - } - - @Override - - public boolean enableDesktopWindowingSizeConstraints() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingSizeConstraints; - - } - - @Override - - public boolean enableDesktopWindowingTaskLimit() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingTaskLimit; - - } - - @Override - - public boolean enableDesktopWindowingTaskbarRunningApps() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingTaskbarRunningApps; - - } - - @Override - - public boolean enableDesktopWindowingWallpaperActivity() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableDesktopWindowingWallpaperActivity; - - } - - @Override - - public boolean enableScaledResizing() { + public boolean enableBugFixesForSecondaryDisplay() { return false; - } @Override - - public boolean enableTaskStackObserverInShell() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableTaskStackObserverInShell; - } - @Override - - public boolean enableThemedAppHeaders() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableThemedAppHeaders; - - } - - @Override - - public boolean enableWindowingDynamicInitialBounds() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableWindowingDynamicInitialBounds; - - } - - @Override - - public boolean enableWindowingEdgeDragResize() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!lse_desktop_experience_is_cached) { - load_overrides_lse_desktop_experience(); - } - } - return enableWindowingEdgeDragResize; - - } - - @Override - - public boolean enableWmExtensionsForAllFlag() { + public boolean enableCameraCompatForDesktopWindowing() { return true; - } @Override - + + + public boolean enableCameraCompatForDesktopWindowingOptOut() { + return true; + } + + @Override + + + public boolean enableCameraCompatForDesktopWindowingOptOutApi() { + return false; + } + + @Override + + + public boolean enableCameraCompatTrackTaskAndAppBugfix() { + return false; + } + + @Override + + + public boolean enableCaptionCompatInsetConversion() { + return false; + } + + @Override + + + public boolean enableCaptionCompatInsetForceConsumption() { + return true; + } + + @Override + + + public boolean enableCaptionCompatInsetForceConsumptionAlways() { + return true; + } + + @Override + + + public boolean enableCascadingWindows() { + return true; + } + + @Override + + + public boolean enableCompatUiVisibilityStatus() { + return true; + } + + @Override + + + public boolean enableCompatuiSysuiLauncher() { + return false; + } + + @Override + + + public boolean enableConnectedDisplaysDnd() { + return false; + } + + @Override + + + public boolean enableConnectedDisplaysPip() { + return false; + } + + @Override + + + public boolean enableConnectedDisplaysWindowDrag() { + return false; + } + + @Override + + + public boolean enableDesktopAppHandleAnimation() { + return true; + } + + @Override + + + public boolean enableDesktopAppLaunchAlttabTransitions() { + return false; + } + + @Override + + + public boolean enableDesktopAppLaunchAlttabTransitionsBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopAppLaunchTransitions() { + return false; + } + + @Override + + + public boolean enableDesktopAppLaunchTransitionsBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopCloseShortcutBugfix() { + return false; + } + + @Override + + + public boolean enableDesktopCloseTaskAnimationInDtcBugfix() { + return false; + } + + @Override + + + public boolean enableDesktopImeBugfix() { + return false; + } + + @Override + + + public boolean enableDesktopImmersiveDragBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopIndicatorInSeparateThreadBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopModeThroughDevOption() { + return false; + } + + @Override + + + public boolean enableDesktopOpeningDeeplinkMinimizeAnimationBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopRecentsTransitionsCornersBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopSwipeBackMinimizeAnimationBugfix() { + return false; + } + + @Override + + + public boolean enableDesktopSystemDialogsTransitions() { + return true; + } + + @Override + + + public boolean enableDesktopTabTearingMinimizeAnimationBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopTaskbarOnFreeformDisplays() { + return false; + } + + @Override + + + public boolean enableDesktopTrampolineCloseAnimationBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopWallpaperActivityForSystemUser() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingAppHandleEducation() { + return false; + } + + @Override + + + public boolean enableDesktopWindowingAppToWeb() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingAppToWebEducation() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingAppToWebEducationIntegration() { + return false; + } + + @Override + + + public boolean enableDesktopWindowingBackNavigation() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingEnterTransitionBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingEnterTransitions() { + return false; + } + + @Override + + + public boolean enableDesktopWindowingExitByMinimizeTransitionBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingExitTransitions() { + return false; + } + + @Override + + + public boolean enableDesktopWindowingExitTransitionsBugfix() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingHsum() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingImmersiveHandleHiding() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingModalsPolicy() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingMode() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingMultiInstanceFeatures() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingPersistence() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingPip() { + return false; + } + + @Override + + + public boolean enableDesktopWindowingQuickSwitch() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingScvhCacheBugFix() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingSizeConstraints() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingTaskLimit() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingTaskbarRunningApps() { + return true; + } + + @Override + + + public boolean enableDesktopWindowingTransitions() { + return false; + } + + @Override + + + public boolean enableDesktopWindowingWallpaperActivity() { + return true; + } + + @Override + + + public boolean enableDeviceStateAutoRotateSettingLogging() { + return false; + } + + @Override + + + public boolean enableDeviceStateAutoRotateSettingRefactor() { + return false; + } + + @Override + + + public boolean enableDisplayDisconnectInteraction() { + return false; + } + + @Override + + + public boolean enableDisplayFocusInShellTransitions() { + return false; + } + + @Override + + + public boolean enableDisplayReconnectInteraction() { + return false; + } + + @Override + + + public boolean enableDisplayWindowingModeSwitching() { + return false; + } + + @Override + + + public boolean enableDragResizeSetUpInBgThread() { + return true; + } + + @Override + + + public boolean enableDragToDesktopIncomingTransitionsBugfix() { + return true; + } + + @Override + + + public boolean enableDragToMaximize() { + return false; + } + + @Override + + + public boolean enableDynamicRadiusComputationBugfix() { + return false; + } + + @Override + + + public boolean enableFullScreenWindowOnRemovingSplitScreenStageBugfix() { + return true; + } + + @Override + + + public boolean enableFullyImmersiveInDesktop() { + return true; + } + + @Override + + + public boolean enableHandleInputFix() { + return true; + } + + @Override + + + public boolean enableHoldToDragAppHandle() { + return true; + } + + @Override + + + public boolean enableInputLayerTransitionFix() { + return true; + } + + @Override + + + public boolean enableMinimizeButton() { + return true; + } + + @Override + + + public boolean enableModalsFullscreenWithPermission() { + return true; + } + + @Override + + + public boolean enableMoveToNextDisplayShortcut() { + return false; + } + + @Override + + + public boolean enableMultiDisplaySplit() { + return false; + } + + @Override + + + public boolean enableMultidisplayTrackpadBackGesture() { + return false; + } + + @Override + + + public boolean enableMultipleDesktopsBackend() { + return false; + } + + @Override + + + public boolean enableMultipleDesktopsFrontend() { + return false; + } + + @Override + + + public boolean enableNonDefaultDisplaySplit() { + return false; + } + + @Override + + + public boolean enableOpaqueBackgroundForTransparentWindows() { + return true; + } + + @Override + + + public boolean enablePerDisplayDesktopWallpaperActivity() { + return false; + } + + @Override + + + public boolean enablePerDisplayPackageContextCacheInStatusbarNotif() { + return false; + } + + @Override + + + public boolean enablePersistingDisplaySizeForConnectedDisplays() { + return false; + } + + @Override + + + public boolean enablePresentationForConnectedDisplays() { + return false; + } + + @Override + + + public boolean enableProjectedDisplayDesktopMode() { + return false; + } + + @Override + + + public boolean enableQuickswitchDesktopSplitBugfix() { + return true; + } + + @Override + + + public boolean enableRequestFullscreenBugfix() { + return true; + } + + @Override + + + public boolean enableResizingMetrics() { + return true; + } + + @Override + + + public boolean enableRestartMenuForConnectedDisplays() { + return false; + } + + @Override + + + public boolean enableRestoreToPreviousSizeFromDesktopImmersive() { + return true; + } + + @Override + + + public boolean enableShellInitialBoundsRegressionBugFix() { + return true; + } + + @Override + + + public boolean enableSizeCompatModeImprovementsForConnectedDisplays() { + return false; + } + + @Override + + + public boolean enableStartLaunchTransitionFromTaskbarBugfix() { + return true; + } + + @Override + + + public boolean enableTaskResizingKeyboardShortcuts() { + return true; + } + + @Override + + + public boolean enableTaskStackObserverInShell() { + return true; + } + + @Override + + + public boolean enableTaskbarConnectedDisplays() { + return false; + } + + @Override + + + public boolean enableTaskbarOverflow() { + return false; + } + + @Override + + + public boolean enableTaskbarRecentsLayoutTransition() { + return true; + } + + @Override + + + public boolean enableThemedAppHeaders() { + return true; + } + + @Override + + + public boolean enableTileResizing() { + return false; + } + + @Override + + + public boolean enableTopVisibleRootTaskPerUserTracking() { + return true; + } + + @Override + + + public boolean enableVisualIndicatorInTransitionBugfix() { + return true; + } + + @Override + + + public boolean enableWindowContextResourcesUpdateOnConfigChange() { + return true; + } + + @Override + + + public boolean enableWindowingDynamicInitialBounds() { + return true; + } + + @Override + + + public boolean enableWindowingEdgeDragResize() { + return true; + } + + @Override + + + public boolean enableWindowingScaledResizing() { + return true; + } + + @Override + + + public boolean enableWindowingTransitionHandlersObservers() { + return false; + } + + @Override + + public boolean enforceEdgeToEdge() { return true; - } @Override - + + + public boolean ensureKeyguardDoesTransitionStarting() { + return false; + } + + @Override + + public boolean ensureWallpaperInTransitions() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_frontend_is_cached) { - load_overrides_windowing_frontend(); - } - } - return ensureWallpaperInTransitions; - - } - - @Override - - public boolean explicitRefreshRateHints() { return true; - } @Override - + + + public boolean ensureWallpaperInWearTransitions() { + return true; + } + + @Override + + + public boolean enterDesktopByDefaultOnFreeformDisplays() { + return false; + } + + @Override + + + public boolean excludeCaptionFromAppBounds() { + return true; + } + + @Override + + + public boolean excludeDrawingAppThemeSnapshotFromLock() { + return true; + } + + @Override + + + public boolean excludeTaskFromRecents() { + return false; + } + + @Override + + public boolean fifoPriorityForMajorUiProcesses() { return false; - } @Override - - public boolean fixNoContainerUpdateWithoutResize() { + + + public boolean fixHideOverlayApi() { + return true; + } + + @Override + + + public boolean fixLayoutExistingTask() { + return true; + } + + @Override + + + public boolean fixViewRootCallTrace() { return false; - } @Override - - public boolean fixPipRestoreToOverlay() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return fixPipRestoreToOverlay; + + public boolean forceCloseTopTransparentFullscreenTask() { + return false; } @Override - - public boolean fullscreenDimFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return fullscreenDimFlag; + + public boolean formFactorBasedDesktopFirstSwitch() { + return false; } @Override - + + public boolean getDimmerOnClosing() { return true; - } @Override - - public boolean immersiveAppRepositioning() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!large_screen_experiences_app_compat_is_cached) { - load_overrides_large_screen_experiences_app_compat(); - } - } - return immersiveAppRepositioning; + + public boolean ignoreAspectRatioRestrictionsForResizeableFreeformActivities() { + return true; } @Override - - public boolean insetsControlChangedItem() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return insetsControlChangedItem; - } - @Override - - public boolean insetsControlSeq() { + public boolean ignoreCornerRadiusAndShadows() { return false; - } @Override - + + + public boolean includeTopTransparentFullscreenTaskInDesktopHeuristic() { + return true; + } + + @Override + + + public boolean inheritTaskBoundsForTrampolineTaskLaunches() { + return true; + } + + @Override + + public boolean insetsDecoupledConfiguration() { return true; - } @Override - - public boolean introduceSmootherDimmer() { + + + public boolean jankApi() { return true; - } @Override - - public boolean keyguardAppearTransition() { + + + public boolean keepAppWindowHideWhileLocked() { return true; - } @Override - + + + public boolean keyboardShortcutsToSwitchDesks() { + return false; + } + + @Override + + + public boolean keyguardGoingAwayTimeout() { + return true; + } + + @Override + + public boolean letterboxBackgroundWallpaper() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!large_screen_experiences_app_compat_is_cached) { - load_overrides_large_screen_experiences_app_compat(); - } - } - return letterboxBackgroundWallpaper; - + return false; } @Override - + + public boolean movableCutoutConfiguration() { return true; - } @Override - - public boolean moveAnimationOptionsToChange() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return moveAnimationOptionsToChange; + + public boolean moveToExternalDisplayShortcut() { + return false; } @Override - + + public boolean multiCrop() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return multiCrop; - + return true; } @Override - + + public boolean navBarTransparentByDefault() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_frontend_is_cached) { - load_overrides_windowing_frontend(); - } - } - return navBarTransparentByDefault; - + return false; } @Override - + + + public boolean nestedTasksWithIndependentBounds() { + return false; + } + + @Override + + public boolean noConsecutiveVisibilityEvents() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return noConsecutiveVisibilityEvents; - + return true; } @Override - + + + public boolean noDuplicateSurfaceDestroyedEvents() { + return true; + } + + @Override + + public boolean noVisibilityEventOnDisplayStateChange() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!wear_frameworks_is_cached) { - load_overrides_wear_frameworks(); - } - } - return noVisibilityEventOnDisplayStateChange; - + return true; } @Override - + + public boolean offloadColorExtraction() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return offloadColorExtraction; - + return false; } @Override - - public boolean predictiveBackSystemAnims() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!systemui_is_cached) { - load_overrides_systemui(); - } - } - return predictiveBackSystemAnims; + + public boolean portWindowSizeAnimation() { + return false; } @Override - + + + public boolean predictiveBackDefaultEnableSdk36() { + return true; + } + + @Override + + + public boolean predictiveBackPrioritySystemNavigationObserver() { + return true; + } + + @Override + + + public boolean predictiveBackSwipeEdgeNoneApi() { + return true; + } + + @Override + + + public boolean predictiveBackSystemOverrideCallback() { + return true; + } + + @Override + + + public boolean predictiveBackThreeButtonNav() { + return true; + } + + @Override + + + public boolean predictiveBackTimestampApi() { + return true; + } + + @Override + + + public boolean processPriorityPolicyForMultiWindowMode() { + return true; + } + + @Override + + public boolean rearDisplayDisableForceDesktopSystemDecorations() { return true; - } @Override - - public boolean releaseSnapshotAggressively() { - return false; - } - @Override - - public boolean removePrepareSurfaceInPlacement() { + public boolean recordTaskSnapshotsBeforeShutdown() { return true; - } @Override - + + + public boolean reduceChangedExclusionRectsMsgs() { + return false; + } + + @Override + + + public boolean reduceKeyguardTransitions() { + return true; + } + + @Override + + + public boolean reduceTaskSnapshotMemoryUsage() { + return false; + } + + @Override + + + public boolean reduceUnnecessaryMeasure() { + return false; + } + + @Override + + + public boolean relativeInsets() { + return false; + } + + @Override + + + public boolean releaseSnapshotAggressively() { + return true; + } + + @Override + + + public boolean releaseUserAspectRatioWm() { + return true; + } + + @Override + + + public boolean removeActivityStarterDreamCallback() { + return false; + } + + @Override + + + public boolean removeDeferHidingClient() { + return true; + } + + @Override + + + public boolean removeDepartTargetFromMotion() { + return false; + } + + @Override + + + public boolean reparentWindowTokenApi() { + return true; + } + + @Override + + + public boolean respectNonTopVisibleFixedOrientation() { + return true; + } + + @Override + + + public boolean respectOrientationChangeForUnresizeable() { + return true; + } + + @Override + + + public boolean safeRegionLetterboxing() { + return false; + } + + @Override + + + public boolean safeReleaseSnapshotAggressively() { + return false; + } + + @Override + + + public boolean schedulingForNotificationShade() { + return true; + } + + @Override + + + public boolean scrambleSnapshotFileName() { + return false; + } + + @Override + + public boolean screenRecordingCallbacks() { return true; - } @Override - + + + public boolean scrollingFromLetterbox() { + return false; + } + + @Override + + public boolean sdkDesiredPresentTime() { return true; - } @Override - - public boolean secureWindowState() { - return true; - } - @Override - public boolean setScPropertiesInClient() { return false; - } @Override - - public boolean skipSleepingWhenSwitchingDisplay() { + + + public boolean showAppHandleLargeScreens() { + return false; + } + + @Override + + + public boolean showDesktopExperienceDevOption() { + return false; + } + + @Override + + + public boolean showDesktopWindowingDevOption() { return true; - } @Override - + + + public boolean showHomeBehindDesktop() { + return false; + } + + @Override + + + public boolean skipCompatUiEducationInDesktopMode() { + return true; + } + + @Override + + + public boolean skipDecorViewRelayoutWhenClosingBugfix() { + return true; + } + + @Override + + + public boolean supportWidgetIntentsOnConnectedDisplay() { + return false; + } + + @Override + + + public boolean supportsDragAssistantToMultiwindow() { + return true; + } + + @Override + + public boolean supportsMultiInstanceSystemUi() { return true; - } @Override - + + public boolean surfaceControlInputReceiver() { return true; - } @Override - + + public boolean surfaceTrustedOverlay() { return true; - } @Override - + + public boolean syncScreenCapture() { return true; - } @Override - + + + public boolean systemUiPostAnimationEnd() { + return false; + } + + @Override + + public boolean taskFragmentSystemOrganizerFlag() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return taskFragmentSystemOrganizerFlag; - + return true; } @Override - + + + public boolean touchPassThroughOptIn() { + return true; + } + + @Override + + + public boolean trackSystemUiContextBeforeWms() { + return true; + } + + @Override + + public boolean transitReadyTracking() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_frontend_is_cached) { - load_overrides_windowing_frontend(); - } - } - return transitReadyTracking; - + return false; } @Override - + + + public boolean transitTrackerPlumbing() { + return false; + } + + @Override + + public boolean trustedPresentationListenerForWindow() { return true; - } @Override - - public boolean untrustedEmbeddingAnyAppPermission() { + + + public boolean unifyBackNavigationTransition() { return true; - } @Override - + + + public boolean universalResizableByDefault() { + return true; + } + + @Override + + + public boolean untrustedEmbeddingAnyAppPermission() { + return false; + } + + @Override + + public boolean untrustedEmbeddingStateSharing() { return true; - } @Override - + + + public boolean updateDimsWhenWindowShown() { + return false; + } + + @Override + + + public boolean useCachedInsetsForDisplaySwitch() { + return false; + } + + @Override + + + public boolean useRtFrameCallbackForSplashScreenTransfer() { + return true; + } + + @Override + + + public boolean useTasksDimOnly() { + return true; + } + + @Override + + + public boolean useVisibleRequestedForProcessTracker() { + return false; + } + + @Override + + public boolean useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!accessibility_is_cached) { - load_overrides_accessibility(); - } - } - return useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds; - + return false; } @Override - - public boolean userMinAspectRatioAppDefault() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!large_screen_experiences_app_compat_is_cached) { - load_overrides_large_screen_experiences_app_compat(); - } - } - return userMinAspectRatioAppDefault; + + public boolean vdmForceAppUniversalResizableApi() { + return true; } @Override - - public boolean waitForTransitionOnDisplaySwitch() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_frontend_is_cached) { - load_overrides_windowing_frontend(); - } - } - return waitForTransitionOnDisplaySwitch; - } - @Override - public boolean wallpaperOffsetAsync() { return true; - } @Override - - public boolean windowSessionRelayoutInfo() { + + + public boolean wlinfoOncreate() { return true; - - } - - @Override - - public boolean windowTokenConfigThreadSafe() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!windowing_sdk_is_cached) { - load_overrides_windowing_sdk(); - } - } - return windowTokenConfigThreadSafe; - } } - - - diff --git a/flags/src/com/android/window/flags2/Flags.java b/flags/src/com/android/window/flags2/Flags.java index 8888b48ce7..5ad83f821d 100644 --- a/flags/src/com/android/window/flags2/Flags.java +++ b/flags/src/com/android/window/flags2/Flags.java @@ -1,17 +1,20 @@ package com.android.window.flags2; // TODO(b/303773055): Remove the annotation after access issue is resolved. + /** @hide */ public final class Flags { + /** @hide */ + public static final String FLAG_ACTION_MODE_EDGE_TO_EDGE = "com.android.window.flags.action_mode_edge_to_edge"; /** @hide */ public static final String FLAG_ACTIVITY_EMBEDDING_ANIMATION_CUSTOMIZATION_FLAG = "com.android.window.flags.activity_embedding_animation_customization_flag"; /** @hide */ + public static final String FLAG_ACTIVITY_EMBEDDING_DELAY_TASK_FRAGMENT_FINISH_FOR_ACTIVITY_LAUNCH = "com.android.window.flags.activity_embedding_delay_task_fragment_finish_for_activity_launch"; + /** @hide */ public static final String FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG = "com.android.window.flags.activity_embedding_interactive_divider_flag"; /** @hide */ - public static final String FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG = "com.android.window.flags.activity_embedding_overlay_presentation_flag"; + public static final String FLAG_ACTIVITY_EMBEDDING_METRICS = "com.android.window.flags.activity_embedding_metrics"; /** @hide */ - public static final String FLAG_ACTIVITY_SNAPSHOT_BY_DEFAULT = "com.android.window.flags.activity_snapshot_by_default"; - /** @hide */ - public static final String FLAG_ACTIVITY_WINDOW_INFO_FLAG = "com.android.window.flags.activity_window_info_flag"; + public static final String FLAG_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS = "com.android.window.flags.activity_embedding_support_for_connected_displays"; /** @hide */ public static final String FLAG_ALLOW_DISABLE_ACTIVITY_RECORD_INPUT_SINK = "com.android.window.flags.allow_disable_activity_record_input_sink"; /** @hide */ @@ -19,85 +22,211 @@ public final class Flags { /** @hide */ public static final String FLAG_ALLOWS_SCREEN_SIZE_DECOUPLED_FROM_STATUS_BAR_AND_CUTOUT = "com.android.window.flags.allows_screen_size_decoupled_from_status_bar_and_cutout"; /** @hide */ - public static final String FLAG_ALWAYS_DEFER_TRANSITION_WHEN_APPLY_WCT = "com.android.window.flags.always_defer_transition_when_apply_wct"; - /** @hide */ public static final String FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER = "com.android.window.flags.always_draw_magnification_fullscreen_border"; /** @hide */ public static final String FLAG_ALWAYS_UPDATE_WALLPAPER_PERMISSION = "com.android.window.flags.always_update_wallpaper_permission"; /** @hide */ + public static final String FLAG_AOD_TRANSITION = "com.android.window.flags.aod_transition"; + /** @hide */ + public static final String FLAG_APP_COMPAT_ASYNC_RELAYOUT = "com.android.window.flags.app_compat_async_relayout"; + /** @hide */ public static final String FLAG_APP_COMPAT_PROPERTIES_API = "com.android.window.flags.app_compat_properties_api"; /** @hide */ public static final String FLAG_APP_COMPAT_REFACTORING = "com.android.window.flags.app_compat_refactoring"; /** @hide */ + public static final String FLAG_APP_COMPAT_UI_FRAMEWORK = "com.android.window.flags.app_compat_ui_framework"; + /** @hide */ + public static final String FLAG_APP_HANDLE_NO_RELAYOUT_ON_EXCLUSION_CHANGE = "com.android.window.flags.app_handle_no_relayout_on_exclusion_change"; + /** @hide */ + public static final String FLAG_APPLY_LIFECYCLE_ON_PIP_CHANGE = "com.android.window.flags.apply_lifecycle_on_pip_change"; + /** @hide */ + public static final String FLAG_AVOID_REBINDING_INTENTIONALLY_DISCONNECTED_WALLPAPER = "com.android.window.flags.avoid_rebinding_intentionally_disconnected_wallpaper"; + /** @hide */ + public static final String FLAG_BACKUP_AND_RESTORE_FOR_USER_ASPECT_RATIO_SETTINGS = "com.android.window.flags.backup_and_restore_for_user_aspect_ratio_settings"; + /** @hide */ + public static final String FLAG_BAL_ADDITIONAL_LOGGING = "com.android.window.flags.bal_additional_logging"; + /** @hide */ + public static final String FLAG_BAL_ADDITIONAL_START_MODES = "com.android.window.flags.bal_additional_start_modes"; + /** @hide */ + public static final String FLAG_BAL_CLEAR_ALLOWLIST_DURATION = "com.android.window.flags.bal_clear_allowlist_duration"; + /** @hide */ public static final String FLAG_BAL_DONT_BRING_EXISTING_BACKGROUND_TASK_STACK_TO_FG = "com.android.window.flags.bal_dont_bring_existing_background_task_stack_to_fg"; /** @hide */ public static final String FLAG_BAL_IMPROVE_REAL_CALLER_VISIBILITY_CHECK = "com.android.window.flags.bal_improve_real_caller_visibility_check"; /** @hide */ public static final String FLAG_BAL_IMPROVED_METRICS = "com.android.window.flags.bal_improved_metrics"; /** @hide */ - public static final String FLAG_BAL_REQUIRE_OPT_IN_BY_PENDING_INTENT_CREATOR = "com.android.window.flags.bal_require_opt_in_by_pending_intent_creator"; + public static final String FLAG_BAL_REDUCE_GRACE_PERIOD = "com.android.window.flags.bal_reduce_grace_period"; /** @hide */ - public static final String FLAG_BAL_REQUIRE_OPT_IN_SAME_UID = "com.android.window.flags.bal_require_opt_in_same_uid"; + public static final String FLAG_BAL_REQUIRE_OPT_IN_BY_PENDING_INTENT_CREATOR = "com.android.window.flags.bal_require_opt_in_by_pending_intent_creator"; /** @hide */ public static final String FLAG_BAL_RESPECT_APP_SWITCH_STATE_WHEN_CHECK_BOUND_BY_FOREGROUND_UID = "com.android.window.flags.bal_respect_app_switch_state_when_check_bound_by_foreground_uid"; /** @hide */ - public static final String FLAG_BAL_SHOW_TOASTS = "com.android.window.flags.bal_show_toasts"; + public static final String FLAG_BAL_SEND_INTENT_WITH_OPTIONS = "com.android.window.flags.bal_send_intent_with_options"; /** @hide */ public static final String FLAG_BAL_SHOW_TOASTS_BLOCKED = "com.android.window.flags.bal_show_toasts_blocked"; /** @hide */ - public static final String FLAG_BLAST_SYNC_NOTIFICATION_SHADE_ON_DISPLAY_SWITCH = "com.android.window.flags.blast_sync_notification_shade_on_display_switch"; + public static final String FLAG_BAL_STRICT_MODE_GRACE_PERIOD = "com.android.window.flags.bal_strict_mode_grace_period"; /** @hide */ - public static final String FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG = "com.android.window.flags.bundle_client_transaction_flag"; + public static final String FLAG_BAL_STRICT_MODE_RO = "com.android.window.flags.bal_strict_mode_ro"; + /** @hide */ + public static final String FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY = "com.android.window.flags.better_support_non_match_parent_activity"; + /** @hide */ + public static final String FLAG_CACHE_WINDOW_STYLE = "com.android.window.flags.cache_window_style"; /** @hide */ public static final String FLAG_CAMERA_COMPAT_FOR_FREEFORM = "com.android.window.flags.camera_compat_for_freeform"; /** @hide */ + public static final String FLAG_CAMERA_COMPAT_FULLSCREEN_PICK_SAME_TASK_ACTIVITY = "com.android.window.flags.camera_compat_fullscreen_pick_same_task_activity"; + /** @hide */ + public static final String FLAG_CHECK_DISABLED_SNAPSHOTS_IN_TASK_PERSISTER = "com.android.window.flags.check_disabled_snapshots_in_task_persister"; + /** @hide */ + public static final String FLAG_CLEANUP_DISPATCH_PENDING_TRANSACTIONS_REMOTE_EXCEPTION = "com.android.window.flags.cleanup_dispatch_pending_transactions_remote_exception"; + /** @hide */ + public static final String FLAG_CLEAR_SYSTEM_VIBRATOR = "com.android.window.flags.clear_system_vibrator"; + /** @hide */ public static final String FLAG_CLOSE_TO_SQUARE_CONFIG_INCLUDES_STATUS_BAR = "com.android.window.flags.close_to_square_config_includes_status_bar"; /** @hide */ + public static final String FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE = "com.android.window.flags.condense_configuration_change_for_simple_mode"; + /** @hide */ public static final String FLAG_CONFIGURABLE_FONT_SCALE_DEFAULT = "com.android.window.flags.configurable_font_scale_default"; /** @hide */ public static final String FLAG_COVER_DISPLAY_OPT_IN = "com.android.window.flags.cover_display_opt_in"; /** @hide */ - public static final String FLAG_DEFER_DISPLAY_UPDATES = "com.android.window.flags.defer_display_updates"; - /** @hide */ public static final String FLAG_DELAY_NOTIFICATION_TO_MAGNIFICATION_WHEN_RECENTS_WINDOW_TO_FRONT_TRANSITION = "com.android.window.flags.delay_notification_to_magnification_when_recents_window_to_front_transition"; /** @hide */ + public static final String FLAG_DELEGATE_BACK_GESTURE_TO_SHELL = "com.android.window.flags.delegate_back_gesture_to_shell"; + /** @hide */ public static final String FLAG_DELEGATE_UNHANDLED_DRAGS = "com.android.window.flags.delegate_unhandled_drags"; /** @hide */ public static final String FLAG_DELETE_CAPTURE_DISPLAY = "com.android.window.flags.delete_capture_display"; /** @hide */ public static final String FLAG_DENSITY_390_API = "com.android.window.flags.density_390_api"; /** @hide */ - public static final String FLAG_DISABLE_OBJECT_POOL = "com.android.window.flags.disable_object_pool"; + public static final String FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX = "com.android.window.flags.disable_desktop_launch_params_outside_desktop_bug_fix"; /** @hide */ - public static final String FLAG_DISABLE_THIN_LETTERBOXING_POLICY = "com.android.window.flags.disable_thin_letterboxing_policy"; + public static final String FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING = "com.android.window.flags.disable_non_resizable_app_snap_resizing"; + /** @hide */ + public static final String FLAG_DISABLE_OPT_OUT_EDGE_TO_EDGE = "com.android.window.flags.disable_opt_out_edge_to_edge"; /** @hide */ public static final String FLAG_DO_NOT_CHECK_INTERSECTION_WHEN_NON_MAGNIFIABLE_WINDOW_TRANSITIONS = "com.android.window.flags.do_not_check_intersection_when_non_magnifiable_window_transitions"; /** @hide */ - public static final String FLAG_DRAW_SNAPSHOT_ASPECT_RATIO_MATCH = "com.android.window.flags.draw_snapshot_aspect_ratio_match"; + public static final String FLAG_EARLY_LAUNCH_HINT = "com.android.window.flags.early_launch_hint"; /** @hide */ public static final String FLAG_EDGE_TO_EDGE_BY_DEFAULT = "com.android.window.flags.edge_to_edge_by_default"; /** @hide */ - public static final String FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG = "com.android.window.flags.embedded_activity_back_nav_flag"; + public static final String FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS = "com.android.window.flags.enable_accessible_custom_headers"; /** @hide */ - public static final String FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR = "com.android.window.flags.enable_additional_windows_above_status_bar"; + public static final String FLAG_ENABLE_ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS = "com.android.window.flags.enable_activity_embedding_support_for_connected_displays"; /** @hide */ public static final String FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY = "com.android.window.flags.enable_app_header_with_task_density"; /** @hide */ + public static final String FLAG_ENABLE_BORDER_SETTINGS = "com.android.window.flags.enable_border_settings"; + /** @hide */ public static final String FLAG_ENABLE_BUFFER_TRANSFORM_HINT_FROM_DISPLAY = "com.android.window.flags.enable_buffer_transform_hint_from_display"; /** @hide */ + public static final String FLAG_ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY = "com.android.window.flags.enable_bug_fixes_for_secondary_display"; + /** @hide */ public static final String FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING = "com.android.window.flags.enable_camera_compat_for_desktop_windowing"; /** @hide */ + public static final String FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT = "com.android.window.flags.enable_camera_compat_for_desktop_windowing_opt_out"; + /** @hide */ + public static final String FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT_API = "com.android.window.flags.enable_camera_compat_for_desktop_windowing_opt_out_api"; + /** @hide */ + public static final String FLAG_ENABLE_CAMERA_COMPAT_TRACK_TASK_AND_APP_BUGFIX = "com.android.window.flags.enable_camera_compat_track_task_and_app_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_CAPTION_COMPAT_INSET_CONVERSION = "com.android.window.flags.enable_caption_compat_inset_conversion"; + /** @hide */ + public static final String FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION = "com.android.window.flags.enable_caption_compat_inset_force_consumption"; + /** @hide */ + public static final String FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS = "com.android.window.flags.enable_caption_compat_inset_force_consumption_always"; + /** @hide */ + public static final String FLAG_ENABLE_CASCADING_WINDOWS = "com.android.window.flags.enable_cascading_windows"; + /** @hide */ + public static final String FLAG_ENABLE_COMPAT_UI_VISIBILITY_STATUS = "com.android.window.flags.enable_compat_ui_visibility_status"; + /** @hide */ public static final String FLAG_ENABLE_COMPATUI_SYSUI_LAUNCHER = "com.android.window.flags.enable_compatui_sysui_launcher"; /** @hide */ + public static final String FLAG_ENABLE_CONNECTED_DISPLAYS_DND = "com.android.window.flags.enable_connected_displays_dnd"; + /** @hide */ + public static final String FLAG_ENABLE_CONNECTED_DISPLAYS_PIP = "com.android.window.flags.enable_connected_displays_pip"; + /** @hide */ + public static final String FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG = "com.android.window.flags.enable_connected_displays_window_drag"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_APP_HANDLE_ANIMATION = "com.android.window.flags.enable_desktop_app_handle_animation"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS = "com.android.window.flags.enable_desktop_app_launch_alttab_transitions"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX = "com.android.window.flags.enable_desktop_app_launch_alttab_transitions_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS = "com.android.window.flags.enable_desktop_app_launch_transitions"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX = "com.android.window.flags.enable_desktop_app_launch_transitions_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX = "com.android.window.flags.enable_desktop_close_shortcut_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_CLOSE_TASK_ANIMATION_IN_DTC_BUGFIX = "com.android.window.flags.enable_desktop_close_task_animation_in_dtc_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_IME_BUGFIX = "com.android.window.flags.enable_desktop_ime_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX = "com.android.window.flags.enable_desktop_immersive_drag_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX = "com.android.window.flags.enable_desktop_indicator_in_separate_thread_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION = "com.android.window.flags.enable_desktop_mode_through_dev_option"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX = "com.android.window.flags.enable_desktop_opening_deeplink_minimize_animation_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX = "com.android.window.flags.enable_desktop_recents_transitions_corners_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_SWIPE_BACK_MINIMIZE_ANIMATION_BUGFIX = "com.android.window.flags.enable_desktop_swipe_back_minimize_animation_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS = "com.android.window.flags.enable_desktop_system_dialogs_transitions"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX = "com.android.window.flags.enable_desktop_tab_tearing_minimize_animation_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_TASKBAR_ON_FREEFORM_DISPLAYS = "com.android.window.flags.enable_desktop_taskbar_on_freeform_displays"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX = "com.android.window.flags.enable_desktop_trampoline_close_animation_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER = "com.android.window.flags.enable_desktop_wallpaper_activity_for_system_user"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION = "com.android.window.flags.enable_desktop_windowing_app_handle_education"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB = "com.android.window.flags.enable_desktop_windowing_app_to_web"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION = "com.android.window.flags.enable_desktop_windowing_app_to_web_education"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION_INTEGRATION = "com.android.window.flags.enable_desktop_windowing_app_to_web_education_integration"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION = "com.android.window.flags.enable_desktop_windowing_back_navigation"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITION_BUGFIX = "com.android.window.flags.enable_desktop_windowing_enter_transition_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS = "com.android.window.flags.enable_desktop_windowing_enter_transitions"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX = "com.android.window.flags.enable_desktop_windowing_exit_by_minimize_transition_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS = "com.android.window.flags.enable_desktop_windowing_exit_transitions"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX = "com.android.window.flags.enable_desktop_windowing_exit_transitions_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_HSUM = "com.android.window.flags.enable_desktop_windowing_hsum"; + /** @hide */ public static final String FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING = "com.android.window.flags.enable_desktop_windowing_immersive_handle_hiding"; /** @hide */ public static final String FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY = "com.android.window.flags.enable_desktop_windowing_modals_policy"; /** @hide */ public static final String FLAG_ENABLE_DESKTOP_WINDOWING_MODE = "com.android.window.flags.enable_desktop_windowing_mode"; /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES = "com.android.window.flags.enable_desktop_windowing_multi_instance_features"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE = "com.android.window.flags.enable_desktop_windowing_persistence"; + /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_PIP = "com.android.window.flags.enable_desktop_windowing_pip"; + /** @hide */ public static final String FLAG_ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH = "com.android.window.flags.enable_desktop_windowing_quick_switch"; /** @hide */ - public static final String FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE = "com.android.window.flags.enable_desktop_windowing_scvh_cache"; + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_SCVH_CACHE_BUG_FIX = "com.android.window.flags.enable_desktop_windowing_scvh_cache_bug_fix"; /** @hide */ public static final String FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS = "com.android.window.flags.enable_desktop_windowing_size_constraints"; /** @hide */ @@ -105,81 +234,257 @@ public final class Flags { /** @hide */ public static final String FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS = "com.android.window.flags.enable_desktop_windowing_taskbar_running_apps"; /** @hide */ + public static final String FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS = "com.android.window.flags.enable_desktop_windowing_transitions"; + /** @hide */ public static final String FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY = "com.android.window.flags.enable_desktop_windowing_wallpaper_activity"; /** @hide */ - public static final String FLAG_ENABLE_SCALED_RESIZING = "com.android.window.flags.enable_scaled_resizing"; + public static final String FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_LOGGING = "com.android.window.flags.enable_device_state_auto_rotate_setting_logging"; + /** @hide */ + public static final String FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR = "com.android.window.flags.enable_device_state_auto_rotate_setting_refactor"; + /** @hide */ + public static final String FLAG_ENABLE_DISPLAY_DISCONNECT_INTERACTION = "com.android.window.flags.enable_display_disconnect_interaction"; + /** @hide */ + public static final String FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS = "com.android.window.flags.enable_display_focus_in_shell_transitions"; + /** @hide */ + public static final String FLAG_ENABLE_DISPLAY_RECONNECT_INTERACTION = "com.android.window.flags.enable_display_reconnect_interaction"; + /** @hide */ + public static final String FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING = "com.android.window.flags.enable_display_windowing_mode_switching"; + /** @hide */ + public static final String FLAG_ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD = "com.android.window.flags.enable_drag_resize_set_up_in_bg_thread"; + /** @hide */ + public static final String FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX = "com.android.window.flags.enable_drag_to_desktop_incoming_transitions_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_DRAG_TO_MAXIMIZE = "com.android.window.flags.enable_drag_to_maximize"; + /** @hide */ + public static final String FLAG_ENABLE_DYNAMIC_RADIUS_COMPUTATION_BUGFIX = "com.android.window.flags.enable_dynamic_radius_computation_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_FULL_SCREEN_WINDOW_ON_REMOVING_SPLIT_SCREEN_STAGE_BUGFIX = "com.android.window.flags.enable_full_screen_window_on_removing_split_screen_stage_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP = "com.android.window.flags.enable_fully_immersive_in_desktop"; + /** @hide */ + public static final String FLAG_ENABLE_HANDLE_INPUT_FIX = "com.android.window.flags.enable_handle_input_fix"; + /** @hide */ + public static final String FLAG_ENABLE_HOLD_TO_DRAG_APP_HANDLE = "com.android.window.flags.enable_hold_to_drag_app_handle"; + /** @hide */ + public static final String FLAG_ENABLE_INPUT_LAYER_TRANSITION_FIX = "com.android.window.flags.enable_input_layer_transition_fix"; + /** @hide */ + public static final String FLAG_ENABLE_MINIMIZE_BUTTON = "com.android.window.flags.enable_minimize_button"; + /** @hide */ + public static final String FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION = "com.android.window.flags.enable_modals_fullscreen_with_permission"; + /** @hide */ + public static final String FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT = "com.android.window.flags.enable_move_to_next_display_shortcut"; + /** @hide */ + public static final String FLAG_ENABLE_MULTI_DISPLAY_SPLIT = "com.android.window.flags.enable_multi_display_split"; + /** @hide */ + public static final String FLAG_ENABLE_MULTIDISPLAY_TRACKPAD_BACK_GESTURE = "com.android.window.flags.enable_multidisplay_trackpad_back_gesture"; + /** @hide */ + public static final String FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND = "com.android.window.flags.enable_multiple_desktops_backend"; + /** @hide */ + public static final String FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND = "com.android.window.flags.enable_multiple_desktops_frontend"; + /** @hide */ + public static final String FLAG_ENABLE_NON_DEFAULT_DISPLAY_SPLIT = "com.android.window.flags.enable_non_default_display_split"; + /** @hide */ + public static final String FLAG_ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS = "com.android.window.flags.enable_opaque_background_for_transparent_windows"; + /** @hide */ + public static final String FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY = "com.android.window.flags.enable_per_display_desktop_wallpaper_activity"; + /** @hide */ + public static final String FLAG_ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF = "com.android.window.flags.enable_per_display_package_context_cache_in_statusbar_notif"; + /** @hide */ + public static final String FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS = "com.android.window.flags.enable_persisting_display_size_for_connected_displays"; + /** @hide */ + public static final String FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS = "com.android.window.flags.enable_presentation_for_connected_displays"; + /** @hide */ + public static final String FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE = "com.android.window.flags.enable_projected_display_desktop_mode"; + /** @hide */ + public static final String FLAG_ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX = "com.android.window.flags.enable_quickswitch_desktop_split_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_REQUEST_FULLSCREEN_BUGFIX = "com.android.window.flags.enable_request_fullscreen_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_RESIZING_METRICS = "com.android.window.flags.enable_resizing_metrics"; + /** @hide */ + public static final String FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS = "com.android.window.flags.enable_restart_menu_for_connected_displays"; + /** @hide */ + public static final String FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE = "com.android.window.flags.enable_restore_to_previous_size_from_desktop_immersive"; + /** @hide */ + public static final String FLAG_ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX = "com.android.window.flags.enable_shell_initial_bounds_regression_bug_fix"; + /** @hide */ + public static final String FLAG_ENABLE_SIZE_COMPAT_MODE_IMPROVEMENTS_FOR_CONNECTED_DISPLAYS = "com.android.window.flags.enable_size_compat_mode_improvements_for_connected_displays"; + /** @hide */ + public static final String FLAG_ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX = "com.android.window.flags.enable_start_launch_transition_from_taskbar_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS = "com.android.window.flags.enable_task_resizing_keyboard_shortcuts"; /** @hide */ public static final String FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL = "com.android.window.flags.enable_task_stack_observer_in_shell"; /** @hide */ + public static final String FLAG_ENABLE_TASKBAR_CONNECTED_DISPLAYS = "com.android.window.flags.enable_taskbar_connected_displays"; + /** @hide */ + public static final String FLAG_ENABLE_TASKBAR_OVERFLOW = "com.android.window.flags.enable_taskbar_overflow"; + /** @hide */ + public static final String FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION = "com.android.window.flags.enable_taskbar_recents_layout_transition"; + /** @hide */ public static final String FLAG_ENABLE_THEMED_APP_HEADERS = "com.android.window.flags.enable_themed_app_headers"; /** @hide */ + public static final String FLAG_ENABLE_TILE_RESIZING = "com.android.window.flags.enable_tile_resizing"; + /** @hide */ + public static final String FLAG_ENABLE_TOP_VISIBLE_ROOT_TASK_PER_USER_TRACKING = "com.android.window.flags.enable_top_visible_root_task_per_user_tracking"; + /** @hide */ + public static final String FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX = "com.android.window.flags.enable_visual_indicator_in_transition_bugfix"; + /** @hide */ + public static final String FLAG_ENABLE_WINDOW_CONTEXT_RESOURCES_UPDATE_ON_CONFIG_CHANGE = "com.android.window.flags.enable_window_context_resources_update_on_config_change"; + /** @hide */ public static final String FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS = "com.android.window.flags.enable_windowing_dynamic_initial_bounds"; /** @hide */ public static final String FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE = "com.android.window.flags.enable_windowing_edge_drag_resize"; /** @hide */ - public static final String FLAG_ENABLE_WM_EXTENSIONS_FOR_ALL_FLAG = "com.android.window.flags.enable_wm_extensions_for_all_flag"; + public static final String FLAG_ENABLE_WINDOWING_SCALED_RESIZING = "com.android.window.flags.enable_windowing_scaled_resizing"; + /** @hide */ + public static final String FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS = "com.android.window.flags.enable_windowing_transition_handlers_observers"; /** @hide */ public static final String FLAG_ENFORCE_EDGE_TO_EDGE = "com.android.window.flags.enforce_edge_to_edge"; /** @hide */ + public static final String FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING = "com.android.window.flags.ensure_keyguard_does_transition_starting"; + /** @hide */ public static final String FLAG_ENSURE_WALLPAPER_IN_TRANSITIONS = "com.android.window.flags.ensure_wallpaper_in_transitions"; /** @hide */ - public static final String FLAG_EXPLICIT_REFRESH_RATE_HINTS = "com.android.window.flags.explicit_refresh_rate_hints"; + public static final String FLAG_ENSURE_WALLPAPER_IN_WEAR_TRANSITIONS = "com.android.window.flags.ensure_wallpaper_in_wear_transitions"; + /** @hide */ + public static final String FLAG_ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS = "com.android.window.flags.enter_desktop_by_default_on_freeform_displays"; + /** @hide */ + public static final String FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS = "com.android.window.flags.exclude_caption_from_app_bounds"; + /** @hide */ + public static final String FLAG_EXCLUDE_DRAWING_APP_THEME_SNAPSHOT_FROM_LOCK = "com.android.window.flags.exclude_drawing_app_theme_snapshot_from_lock"; + /** @hide */ + public static final String FLAG_EXCLUDE_TASK_FROM_RECENTS = "com.android.window.flags.exclude_task_from_recents"; /** @hide */ public static final String FLAG_FIFO_PRIORITY_FOR_MAJOR_UI_PROCESSES = "com.android.window.flags.fifo_priority_for_major_ui_processes"; /** @hide */ - public static final String FLAG_FIX_NO_CONTAINER_UPDATE_WITHOUT_RESIZE = "com.android.window.flags.fix_no_container_update_without_resize"; + public static final String FLAG_FIX_HIDE_OVERLAY_API = "com.android.window.flags.fix_hide_overlay_api"; /** @hide */ - public static final String FLAG_FIX_PIP_RESTORE_TO_OVERLAY = "com.android.window.flags.fix_pip_restore_to_overlay"; + public static final String FLAG_FIX_LAYOUT_EXISTING_TASK = "com.android.window.flags.fix_layout_existing_task"; /** @hide */ - public static final String FLAG_FULLSCREEN_DIM_FLAG = "com.android.window.flags.fullscreen_dim_flag"; + public static final String FLAG_FIX_VIEW_ROOT_CALL_TRACE = "com.android.window.flags.fix_view_root_call_trace"; + /** @hide */ + public static final String FLAG_FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK = "com.android.window.flags.force_close_top_transparent_fullscreen_task"; + /** @hide */ + public static final String FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH = "com.android.window.flags.form_factor_based_desktop_first_switch"; /** @hide */ public static final String FLAG_GET_DIMMER_ON_CLOSING = "com.android.window.flags.get_dimmer_on_closing"; /** @hide */ - public static final String FLAG_IMMERSIVE_APP_REPOSITIONING = "com.android.window.flags.immersive_app_repositioning"; + public static final String FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES = "com.android.window.flags.ignore_aspect_ratio_restrictions_for_resizeable_freeform_activities"; /** @hide */ - public static final String FLAG_INSETS_CONTROL_CHANGED_ITEM = "com.android.window.flags.insets_control_changed_item"; + public static final String FLAG_IGNORE_CORNER_RADIUS_AND_SHADOWS = "com.android.window.flags.ignore_corner_radius_and_shadows"; /** @hide */ - public static final String FLAG_INSETS_CONTROL_SEQ = "com.android.window.flags.insets_control_seq"; + public static final String FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC = "com.android.window.flags.include_top_transparent_fullscreen_task_in_desktop_heuristic"; + /** @hide */ + public static final String FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES = "com.android.window.flags.inherit_task_bounds_for_trampoline_task_launches"; /** @hide */ public static final String FLAG_INSETS_DECOUPLED_CONFIGURATION = "com.android.window.flags.insets_decoupled_configuration"; /** @hide */ - public static final String FLAG_INTRODUCE_SMOOTHER_DIMMER = "com.android.window.flags.introduce_smoother_dimmer"; + public static final String FLAG_JANK_API = "com.android.window.flags.jank_api"; /** @hide */ - public static final String FLAG_KEYGUARD_APPEAR_TRANSITION = "com.android.window.flags.keyguard_appear_transition"; + public static final String FLAG_KEEP_APP_WINDOW_HIDE_WHILE_LOCKED = "com.android.window.flags.keep_app_window_hide_while_locked"; + /** @hide */ + public static final String FLAG_KEYBOARD_SHORTCUTS_TO_SWITCH_DESKS = "com.android.window.flags.keyboard_shortcuts_to_switch_desks"; + /** @hide */ + public static final String FLAG_KEYGUARD_GOING_AWAY_TIMEOUT = "com.android.window.flags.keyguard_going_away_timeout"; /** @hide */ public static final String FLAG_LETTERBOX_BACKGROUND_WALLPAPER = "com.android.window.flags.letterbox_background_wallpaper"; /** @hide */ public static final String FLAG_MOVABLE_CUTOUT_CONFIGURATION = "com.android.window.flags.movable_cutout_configuration"; /** @hide */ - public static final String FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE = "com.android.window.flags.move_animation_options_to_change"; + public static final String FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT = "com.android.window.flags.move_to_external_display_shortcut"; /** @hide */ public static final String FLAG_MULTI_CROP = "com.android.window.flags.multi_crop"; /** @hide */ public static final String FLAG_NAV_BAR_TRANSPARENT_BY_DEFAULT = "com.android.window.flags.nav_bar_transparent_by_default"; /** @hide */ + public static final String FLAG_NESTED_TASKS_WITH_INDEPENDENT_BOUNDS = "com.android.window.flags.nested_tasks_with_independent_bounds"; + /** @hide */ public static final String FLAG_NO_CONSECUTIVE_VISIBILITY_EVENTS = "com.android.window.flags.no_consecutive_visibility_events"; /** @hide */ + public static final String FLAG_NO_DUPLICATE_SURFACE_DESTROYED_EVENTS = "com.android.window.flags.no_duplicate_surface_destroyed_events"; + /** @hide */ public static final String FLAG_NO_VISIBILITY_EVENT_ON_DISPLAY_STATE_CHANGE = "com.android.window.flags.no_visibility_event_on_display_state_change"; /** @hide */ public static final String FLAG_OFFLOAD_COLOR_EXTRACTION = "com.android.window.flags.offload_color_extraction"; /** @hide */ - public static final String FLAG_PREDICTIVE_BACK_SYSTEM_ANIMS = "com.android.window.flags.predictive_back_system_anims"; + public static final String FLAG_PORT_WINDOW_SIZE_ANIMATION = "com.android.window.flags.port_window_size_animation"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_DEFAULT_ENABLE_SDK_36 = "com.android.window.flags.predictive_back_default_enable_sdk_36"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_PRIORITY_SYSTEM_NAVIGATION_OBSERVER = "com.android.window.flags.predictive_back_priority_system_navigation_observer"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_SWIPE_EDGE_NONE_API = "com.android.window.flags.predictive_back_swipe_edge_none_api"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_SYSTEM_OVERRIDE_CALLBACK = "com.android.window.flags.predictive_back_system_override_callback"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV = "com.android.window.flags.predictive_back_three_button_nav"; + /** @hide */ + public static final String FLAG_PREDICTIVE_BACK_TIMESTAMP_API = "com.android.window.flags.predictive_back_timestamp_api"; + /** @hide */ + public static final String FLAG_PROCESS_PRIORITY_POLICY_FOR_MULTI_WINDOW_MODE = "com.android.window.flags.process_priority_policy_for_multi_window_mode"; /** @hide */ public static final String FLAG_REAR_DISPLAY_DISABLE_FORCE_DESKTOP_SYSTEM_DECORATIONS = "com.android.window.flags.rear_display_disable_force_desktop_system_decorations"; /** @hide */ + public static final String FLAG_RECORD_TASK_SNAPSHOTS_BEFORE_SHUTDOWN = "com.android.window.flags.record_task_snapshots_before_shutdown"; + /** @hide */ + public static final String FLAG_REDUCE_CHANGED_EXCLUSION_RECTS_MSGS = "com.android.window.flags.reduce_changed_exclusion_rects_msgs"; + /** @hide */ + public static final String FLAG_REDUCE_KEYGUARD_TRANSITIONS = "com.android.window.flags.reduce_keyguard_transitions"; + /** @hide */ + public static final String FLAG_REDUCE_TASK_SNAPSHOT_MEMORY_USAGE = "com.android.window.flags.reduce_task_snapshot_memory_usage"; + /** @hide */ + public static final String FLAG_REDUCE_UNNECESSARY_MEASURE = "com.android.window.flags.reduce_unnecessary_measure"; + /** @hide */ + public static final String FLAG_RELATIVE_INSETS = "com.android.window.flags.relative_insets"; + /** @hide */ public static final String FLAG_RELEASE_SNAPSHOT_AGGRESSIVELY = "com.android.window.flags.release_snapshot_aggressively"; /** @hide */ - public static final String FLAG_REMOVE_PREPARE_SURFACE_IN_PLACEMENT = "com.android.window.flags.remove_prepare_surface_in_placement"; + public static final String FLAG_RELEASE_USER_ASPECT_RATIO_WM = "com.android.window.flags.release_user_aspect_ratio_wm"; + /** @hide */ + public static final String FLAG_REMOVE_ACTIVITY_STARTER_DREAM_CALLBACK = "com.android.window.flags.remove_activity_starter_dream_callback"; + /** @hide */ + public static final String FLAG_REMOVE_DEFER_HIDING_CLIENT = "com.android.window.flags.remove_defer_hiding_client"; + /** @hide */ + public static final String FLAG_REMOVE_DEPART_TARGET_FROM_MOTION = "com.android.window.flags.remove_depart_target_from_motion"; + /** @hide */ + public static final String FLAG_REPARENT_WINDOW_TOKEN_API = "com.android.window.flags.reparent_window_token_api"; + /** @hide */ + public static final String FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION = "com.android.window.flags.respect_non_top_visible_fixed_orientation"; + /** @hide */ + public static final String FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE = "com.android.window.flags.respect_orientation_change_for_unresizeable"; + /** @hide */ + public static final String FLAG_SAFE_REGION_LETTERBOXING = "com.android.window.flags.safe_region_letterboxing"; + /** @hide */ + public static final String FLAG_SAFE_RELEASE_SNAPSHOT_AGGRESSIVELY = "com.android.window.flags.safe_release_snapshot_aggressively"; + /** @hide */ + public static final String FLAG_SCHEDULING_FOR_NOTIFICATION_SHADE = "com.android.window.flags.scheduling_for_notification_shade"; + /** @hide */ + public static final String FLAG_SCRAMBLE_SNAPSHOT_FILE_NAME = "com.android.window.flags.scramble_snapshot_file_name"; /** @hide */ public static final String FLAG_SCREEN_RECORDING_CALLBACKS = "com.android.window.flags.screen_recording_callbacks"; /** @hide */ - public static final String FLAG_SDK_DESIRED_PRESENT_TIME = "com.android.window.flags.sdk_desired_present_time"; + public static final String FLAG_SCROLLING_FROM_LETTERBOX = "com.android.window.flags.scrolling_from_letterbox"; /** @hide */ - public static final String FLAG_SECURE_WINDOW_STATE = "com.android.window.flags.secure_window_state"; + public static final String FLAG_SDK_DESIRED_PRESENT_TIME = "com.android.window.flags.sdk_desired_present_time"; /** @hide */ public static final String FLAG_SET_SC_PROPERTIES_IN_CLIENT = "com.android.window.flags.set_sc_properties_in_client"; /** @hide */ - public static final String FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY = "com.android.window.flags.skip_sleeping_when_switching_display"; + public static final String FLAG_SHOW_APP_HANDLE_LARGE_SCREENS = "com.android.window.flags.show_app_handle_large_screens"; + /** @hide */ + public static final String FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION = "com.android.window.flags.show_desktop_experience_dev_option"; + /** @hide */ + public static final String FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION = "com.android.window.flags.show_desktop_windowing_dev_option"; + /** @hide */ + public static final String FLAG_SHOW_HOME_BEHIND_DESKTOP = "com.android.window.flags.show_home_behind_desktop"; + /** @hide */ + public static final String FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE = "com.android.window.flags.skip_compat_ui_education_in_desktop_mode"; + /** @hide */ + public static final String FLAG_SKIP_DECOR_VIEW_RELAYOUT_WHEN_CLOSING_BUGFIX = "com.android.window.flags.skip_decor_view_relayout_when_closing_bugfix"; + /** @hide */ + public static final String FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY = "com.android.window.flags.support_widget_intents_on_connected_display"; + /** @hide */ + public static final String FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW = "com.android.window.flags.supports_drag_assistant_to_multiwindow"; /** @hide */ public static final String FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = "com.android.window.flags.supports_multi_instance_system_ui"; /** @hide */ @@ -189,442 +494,1899 @@ public final class Flags { /** @hide */ public static final String FLAG_SYNC_SCREEN_CAPTURE = "com.android.window.flags.sync_screen_capture"; /** @hide */ + public static final String FLAG_SYSTEM_UI_POST_ANIMATION_END = "com.android.window.flags.system_ui_post_animation_end"; + /** @hide */ public static final String FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG = "com.android.window.flags.task_fragment_system_organizer_flag"; /** @hide */ + public static final String FLAG_TOUCH_PASS_THROUGH_OPT_IN = "com.android.window.flags.touch_pass_through_opt_in"; + /** @hide */ + public static final String FLAG_TRACK_SYSTEM_UI_CONTEXT_BEFORE_WMS = "com.android.window.flags.track_system_ui_context_before_wms"; + /** @hide */ public static final String FLAG_TRANSIT_READY_TRACKING = "com.android.window.flags.transit_ready_tracking"; /** @hide */ + public static final String FLAG_TRANSIT_TRACKER_PLUMBING = "com.android.window.flags.transit_tracker_plumbing"; + /** @hide */ public static final String FLAG_TRUSTED_PRESENTATION_LISTENER_FOR_WINDOW = "com.android.window.flags.trusted_presentation_listener_for_window"; /** @hide */ + public static final String FLAG_UNIFY_BACK_NAVIGATION_TRANSITION = "com.android.window.flags.unify_back_navigation_transition"; + /** @hide */ + public static final String FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT = "com.android.window.flags.universal_resizable_by_default"; + /** @hide */ public static final String FLAG_UNTRUSTED_EMBEDDING_ANY_APP_PERMISSION = "com.android.window.flags.untrusted_embedding_any_app_permission"; /** @hide */ public static final String FLAG_UNTRUSTED_EMBEDDING_STATE_SHARING = "com.android.window.flags.untrusted_embedding_state_sharing"; /** @hide */ + public static final String FLAG_UPDATE_DIMS_WHEN_WINDOW_SHOWN = "com.android.window.flags.update_dims_when_window_shown"; + /** @hide */ + public static final String FLAG_USE_CACHED_INSETS_FOR_DISPLAY_SWITCH = "com.android.window.flags.use_cached_insets_for_display_switch"; + /** @hide */ + public static final String FLAG_USE_RT_FRAME_CALLBACK_FOR_SPLASH_SCREEN_TRANSFER = "com.android.window.flags.use_rt_frame_callback_for_splash_screen_transfer"; + /** @hide */ + public static final String FLAG_USE_TASKS_DIM_ONLY = "com.android.window.flags.use_tasks_dim_only"; + /** @hide */ + public static final String FLAG_USE_VISIBLE_REQUESTED_FOR_PROCESS_TRACKER = "com.android.window.flags.use_visible_requested_for_process_tracker"; + /** @hide */ public static final String FLAG_USE_WINDOW_ORIGINAL_TOUCHABLE_REGION_WHEN_MAGNIFICATION_RECOMPUTE_BOUNDS = "com.android.window.flags.use_window_original_touchable_region_when_magnification_recompute_bounds"; /** @hide */ - public static final String FLAG_USER_MIN_ASPECT_RATIO_APP_DEFAULT = "com.android.window.flags.user_min_aspect_ratio_app_default"; - /** @hide */ - public static final String FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH = "com.android.window.flags.wait_for_transition_on_display_switch"; + public static final String FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API = "com.android.window.flags.vdm_force_app_universal_resizable_api"; /** @hide */ public static final String FLAG_WALLPAPER_OFFSET_ASYNC = "com.android.window.flags.wallpaper_offset_async"; /** @hide */ - public static final String FLAG_WINDOW_SESSION_RELAYOUT_INFO = "com.android.window.flags.window_session_relayout_info"; - /** @hide */ - public static final String FLAG_WINDOW_TOKEN_CONFIG_THREAD_SAFE = "com.android.window.flags.window_token_config_thread_safe"; - + public static final String FLAG_WLINFO_ONCREATE = "com.android.window.flags.wlinfo_oncreate"; + + + + public static boolean actionModeEdgeToEdge() { + + return FEATURE_FLAGS.actionModeEdgeToEdge(); + } + + + public static boolean activityEmbeddingAnimationCustomizationFlag() { + return FEATURE_FLAGS.activityEmbeddingAnimationCustomizationFlag(); } - + + + + public static boolean activityEmbeddingDelayTaskFragmentFinishForActivityLaunch() { + + return FEATURE_FLAGS.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch(); + } + + + public static boolean activityEmbeddingInteractiveDividerFlag() { + return FEATURE_FLAGS.activityEmbeddingInteractiveDividerFlag(); } - - public static boolean activityEmbeddingOverlayPresentationFlag() { - return FEATURE_FLAGS.activityEmbeddingOverlayPresentationFlag(); + + + + public static boolean activityEmbeddingMetrics() { + + return FEATURE_FLAGS.activityEmbeddingMetrics(); } - - public static boolean activitySnapshotByDefault() { - return FEATURE_FLAGS.activitySnapshotByDefault(); + + + + public static boolean activityEmbeddingSupportForConnectedDisplays() { + + return FEATURE_FLAGS.activityEmbeddingSupportForConnectedDisplays(); } - - public static boolean activityWindowInfoFlag() { - return FEATURE_FLAGS.activityWindowInfoFlag(); - } - + + + public static boolean allowDisableActivityRecordInputSink() { + return FEATURE_FLAGS.allowDisableActivityRecordInputSink(); } - + + + public static boolean allowHideScmButton() { + return FEATURE_FLAGS.allowHideScmButton(); } - + + + public static boolean allowsScreenSizeDecoupledFromStatusBarAndCutout() { + return FEATURE_FLAGS.allowsScreenSizeDecoupledFromStatusBarAndCutout(); } - - public static boolean alwaysDeferTransitionWhenApplyWct() { - return FEATURE_FLAGS.alwaysDeferTransitionWhenApplyWct(); - } - + + + public static boolean alwaysDrawMagnificationFullscreenBorder() { + return FEATURE_FLAGS.alwaysDrawMagnificationFullscreenBorder(); } - + + + public static boolean alwaysUpdateWallpaperPermission() { + return FEATURE_FLAGS.alwaysUpdateWallpaperPermission(); } - + + + + public static boolean aodTransition() { + + return FEATURE_FLAGS.aodTransition(); + } + + + + public static boolean appCompatAsyncRelayout() { + + return FEATURE_FLAGS.appCompatAsyncRelayout(); + } + + + public static boolean appCompatPropertiesApi() { + return FEATURE_FLAGS.appCompatPropertiesApi(); } - + + + public static boolean appCompatRefactoring() { + return FEATURE_FLAGS.appCompatRefactoring(); } - + + + + public static boolean appCompatUiFramework() { + + return FEATURE_FLAGS.appCompatUiFramework(); + } + + + + public static boolean appHandleNoRelayoutOnExclusionChange() { + + return FEATURE_FLAGS.appHandleNoRelayoutOnExclusionChange(); + } + + + + public static boolean applyLifecycleOnPipChange() { + + return FEATURE_FLAGS.applyLifecycleOnPipChange(); + } + + + + public static boolean avoidRebindingIntentionallyDisconnectedWallpaper() { + + return FEATURE_FLAGS.avoidRebindingIntentionallyDisconnectedWallpaper(); + } + + + + public static boolean backupAndRestoreForUserAspectRatioSettings() { + + return FEATURE_FLAGS.backupAndRestoreForUserAspectRatioSettings(); + } + + + + public static boolean balAdditionalLogging() { + + return FEATURE_FLAGS.balAdditionalLogging(); + } + + + + public static boolean balAdditionalStartModes() { + + return FEATURE_FLAGS.balAdditionalStartModes(); + } + + + + public static boolean balClearAllowlistDuration() { + + return FEATURE_FLAGS.balClearAllowlistDuration(); + } + + + public static boolean balDontBringExistingBackgroundTaskStackToFg() { + return FEATURE_FLAGS.balDontBringExistingBackgroundTaskStackToFg(); } - + + + public static boolean balImproveRealCallerVisibilityCheck() { + return FEATURE_FLAGS.balImproveRealCallerVisibilityCheck(); } - + + + public static boolean balImprovedMetrics() { + return FEATURE_FLAGS.balImprovedMetrics(); } - + + + + public static boolean balReduceGracePeriod() { + + return FEATURE_FLAGS.balReduceGracePeriod(); + } + + + public static boolean balRequireOptInByPendingIntentCreator() { + return FEATURE_FLAGS.balRequireOptInByPendingIntentCreator(); } - - public static boolean balRequireOptInSameUid() { - return FEATURE_FLAGS.balRequireOptInSameUid(); - } - + + + public static boolean balRespectAppSwitchStateWhenCheckBoundByForegroundUid() { + return FEATURE_FLAGS.balRespectAppSwitchStateWhenCheckBoundByForegroundUid(); } - - public static boolean balShowToasts() { - return FEATURE_FLAGS.balShowToasts(); + + + + public static boolean balSendIntentWithOptions() { + + return FEATURE_FLAGS.balSendIntentWithOptions(); } - + + + public static boolean balShowToastsBlocked() { + return FEATURE_FLAGS.balShowToastsBlocked(); } - - public static boolean blastSyncNotificationShadeOnDisplaySwitch() { - return FEATURE_FLAGS.blastSyncNotificationShadeOnDisplaySwitch(); + + + + public static boolean balStrictModeGracePeriod() { + + return FEATURE_FLAGS.balStrictModeGracePeriod(); } - - public static boolean bundleClientTransactionFlag() { - return FEATURE_FLAGS.bundleClientTransactionFlag(); + + + + public static boolean balStrictModeRo() { + + return FEATURE_FLAGS.balStrictModeRo(); } - + + + + public static boolean betterSupportNonMatchParentActivity() { + + return FEATURE_FLAGS.betterSupportNonMatchParentActivity(); + } + + + + public static boolean cacheWindowStyle() { + + return FEATURE_FLAGS.cacheWindowStyle(); + } + + + public static boolean cameraCompatForFreeform() { + return FEATURE_FLAGS.cameraCompatForFreeform(); } - + + + + public static boolean cameraCompatFullscreenPickSameTaskActivity() { + + return FEATURE_FLAGS.cameraCompatFullscreenPickSameTaskActivity(); + } + + + + public static boolean checkDisabledSnapshotsInTaskPersister() { + + return FEATURE_FLAGS.checkDisabledSnapshotsInTaskPersister(); + } + + + + public static boolean cleanupDispatchPendingTransactionsRemoteException() { + + return FEATURE_FLAGS.cleanupDispatchPendingTransactionsRemoteException(); + } + + + + public static boolean clearSystemVibrator() { + + return FEATURE_FLAGS.clearSystemVibrator(); + } + + + public static boolean closeToSquareConfigIncludesStatusBar() { + return FEATURE_FLAGS.closeToSquareConfigIncludesStatusBar(); } - + + + + public static boolean condenseConfigurationChangeForSimpleMode() { + + return FEATURE_FLAGS.condenseConfigurationChangeForSimpleMode(); + } + + + public static boolean configurableFontScaleDefault() { + return FEATURE_FLAGS.configurableFontScaleDefault(); } - + + + public static boolean coverDisplayOptIn() { + return FEATURE_FLAGS.coverDisplayOptIn(); } - - public static boolean deferDisplayUpdates() { - return FEATURE_FLAGS.deferDisplayUpdates(); - } - + + + public static boolean delayNotificationToMagnificationWhenRecentsWindowToFrontTransition() { + return FEATURE_FLAGS.delayNotificationToMagnificationWhenRecentsWindowToFrontTransition(); } - + + + + public static boolean delegateBackGestureToShell() { + + return FEATURE_FLAGS.delegateBackGestureToShell(); + } + + + public static boolean delegateUnhandledDrags() { + return FEATURE_FLAGS.delegateUnhandledDrags(); } - + + + public static boolean deleteCaptureDisplay() { + return FEATURE_FLAGS.deleteCaptureDisplay(); } - + + + public static boolean density390Api() { + return FEATURE_FLAGS.density390Api(); } - - public static boolean disableObjectPool() { - return FEATURE_FLAGS.disableObjectPool(); + + + + public static boolean disableDesktopLaunchParamsOutsideDesktopBugFix() { + + return FEATURE_FLAGS.disableDesktopLaunchParamsOutsideDesktopBugFix(); } - - public static boolean disableThinLetterboxingPolicy() { - return FEATURE_FLAGS.disableThinLetterboxingPolicy(); + + + + public static boolean disableNonResizableAppSnapResizing() { + + return FEATURE_FLAGS.disableNonResizableAppSnapResizing(); } - + + + + public static boolean disableOptOutEdgeToEdge() { + + return FEATURE_FLAGS.disableOptOutEdgeToEdge(); + } + + + public static boolean doNotCheckIntersectionWhenNonMagnifiableWindowTransitions() { + return FEATURE_FLAGS.doNotCheckIntersectionWhenNonMagnifiableWindowTransitions(); } - - public static boolean drawSnapshotAspectRatioMatch() { - return FEATURE_FLAGS.drawSnapshotAspectRatioMatch(); + + + + public static boolean earlyLaunchHint() { + + return FEATURE_FLAGS.earlyLaunchHint(); } - + + + public static boolean edgeToEdgeByDefault() { + return FEATURE_FLAGS.edgeToEdgeByDefault(); } - - public static boolean embeddedActivityBackNavFlag() { - return FEATURE_FLAGS.embeddedActivityBackNavFlag(); + + + + public static boolean enableAccessibleCustomHeaders() { + + return FEATURE_FLAGS.enableAccessibleCustomHeaders(); } - - public static boolean enableAdditionalWindowsAboveStatusBar() { - return FEATURE_FLAGS.enableAdditionalWindowsAboveStatusBar(); + + + + public static boolean enableActivityEmbeddingSupportForConnectedDisplays() { + + return FEATURE_FLAGS.enableActivityEmbeddingSupportForConnectedDisplays(); } - + + + public static boolean enableAppHeaderWithTaskDensity() { + return FEATURE_FLAGS.enableAppHeaderWithTaskDensity(); } - + + + + public static boolean enableBorderSettings() { + + return FEATURE_FLAGS.enableBorderSettings(); + } + + + public static boolean enableBufferTransformHintFromDisplay() { + return FEATURE_FLAGS.enableBufferTransformHintFromDisplay(); } - + + + + public static boolean enableBugFixesForSecondaryDisplay() { + + return FEATURE_FLAGS.enableBugFixesForSecondaryDisplay(); + } + + + public static boolean enableCameraCompatForDesktopWindowing() { + return FEATURE_FLAGS.enableCameraCompatForDesktopWindowing(); } - + + + + public static boolean enableCameraCompatForDesktopWindowingOptOut() { + + return FEATURE_FLAGS.enableCameraCompatForDesktopWindowingOptOut(); + } + + + + public static boolean enableCameraCompatForDesktopWindowingOptOutApi() { + + return FEATURE_FLAGS.enableCameraCompatForDesktopWindowingOptOutApi(); + } + + + + public static boolean enableCameraCompatTrackTaskAndAppBugfix() { + + return FEATURE_FLAGS.enableCameraCompatTrackTaskAndAppBugfix(); + } + + + + public static boolean enableCaptionCompatInsetConversion() { + + return FEATURE_FLAGS.enableCaptionCompatInsetConversion(); + } + + + + public static boolean enableCaptionCompatInsetForceConsumption() { + + return FEATURE_FLAGS.enableCaptionCompatInsetForceConsumption(); + } + + + + public static boolean enableCaptionCompatInsetForceConsumptionAlways() { + + return FEATURE_FLAGS.enableCaptionCompatInsetForceConsumptionAlways(); + } + + + + public static boolean enableCascadingWindows() { + + return FEATURE_FLAGS.enableCascadingWindows(); + } + + + + public static boolean enableCompatUiVisibilityStatus() { + + return FEATURE_FLAGS.enableCompatUiVisibilityStatus(); + } + + + public static boolean enableCompatuiSysuiLauncher() { + return FEATURE_FLAGS.enableCompatuiSysuiLauncher(); } - + + + + public static boolean enableConnectedDisplaysDnd() { + + return FEATURE_FLAGS.enableConnectedDisplaysDnd(); + } + + + + public static boolean enableConnectedDisplaysPip() { + + return FEATURE_FLAGS.enableConnectedDisplaysPip(); + } + + + + public static boolean enableConnectedDisplaysWindowDrag() { + + return FEATURE_FLAGS.enableConnectedDisplaysWindowDrag(); + } + + + + public static boolean enableDesktopAppHandleAnimation() { + + return FEATURE_FLAGS.enableDesktopAppHandleAnimation(); + } + + + + public static boolean enableDesktopAppLaunchAlttabTransitions() { + + return FEATURE_FLAGS.enableDesktopAppLaunchAlttabTransitions(); + } + + + + public static boolean enableDesktopAppLaunchAlttabTransitionsBugfix() { + + return FEATURE_FLAGS.enableDesktopAppLaunchAlttabTransitionsBugfix(); + } + + + + public static boolean enableDesktopAppLaunchTransitions() { + + return FEATURE_FLAGS.enableDesktopAppLaunchTransitions(); + } + + + + public static boolean enableDesktopAppLaunchTransitionsBugfix() { + + return FEATURE_FLAGS.enableDesktopAppLaunchTransitionsBugfix(); + } + + + + public static boolean enableDesktopCloseShortcutBugfix() { + + return FEATURE_FLAGS.enableDesktopCloseShortcutBugfix(); + } + + + + public static boolean enableDesktopCloseTaskAnimationInDtcBugfix() { + + return FEATURE_FLAGS.enableDesktopCloseTaskAnimationInDtcBugfix(); + } + + + + public static boolean enableDesktopImeBugfix() { + + return FEATURE_FLAGS.enableDesktopImeBugfix(); + } + + + + public static boolean enableDesktopImmersiveDragBugfix() { + + return FEATURE_FLAGS.enableDesktopImmersiveDragBugfix(); + } + + + + public static boolean enableDesktopIndicatorInSeparateThreadBugfix() { + + return FEATURE_FLAGS.enableDesktopIndicatorInSeparateThreadBugfix(); + } + + + + public static boolean enableDesktopModeThroughDevOption() { + + return FEATURE_FLAGS.enableDesktopModeThroughDevOption(); + } + + + + public static boolean enableDesktopOpeningDeeplinkMinimizeAnimationBugfix() { + + return FEATURE_FLAGS.enableDesktopOpeningDeeplinkMinimizeAnimationBugfix(); + } + + + + public static boolean enableDesktopRecentsTransitionsCornersBugfix() { + + return FEATURE_FLAGS.enableDesktopRecentsTransitionsCornersBugfix(); + } + + + + public static boolean enableDesktopSwipeBackMinimizeAnimationBugfix() { + + return FEATURE_FLAGS.enableDesktopSwipeBackMinimizeAnimationBugfix(); + } + + + + public static boolean enableDesktopSystemDialogsTransitions() { + + return FEATURE_FLAGS.enableDesktopSystemDialogsTransitions(); + } + + + + public static boolean enableDesktopTabTearingMinimizeAnimationBugfix() { + + return FEATURE_FLAGS.enableDesktopTabTearingMinimizeAnimationBugfix(); + } + + + + public static boolean enableDesktopTaskbarOnFreeformDisplays() { + + return FEATURE_FLAGS.enableDesktopTaskbarOnFreeformDisplays(); + } + + + + public static boolean enableDesktopTrampolineCloseAnimationBugfix() { + + return FEATURE_FLAGS.enableDesktopTrampolineCloseAnimationBugfix(); + } + + + + public static boolean enableDesktopWallpaperActivityForSystemUser() { + + return FEATURE_FLAGS.enableDesktopWallpaperActivityForSystemUser(); + } + + + + public static boolean enableDesktopWindowingAppHandleEducation() { + + return FEATURE_FLAGS.enableDesktopWindowingAppHandleEducation(); + } + + + + public static boolean enableDesktopWindowingAppToWeb() { + + return FEATURE_FLAGS.enableDesktopWindowingAppToWeb(); + } + + + + public static boolean enableDesktopWindowingAppToWebEducation() { + + return FEATURE_FLAGS.enableDesktopWindowingAppToWebEducation(); + } + + + + public static boolean enableDesktopWindowingAppToWebEducationIntegration() { + + return FEATURE_FLAGS.enableDesktopWindowingAppToWebEducationIntegration(); + } + + + + public static boolean enableDesktopWindowingBackNavigation() { + + return FEATURE_FLAGS.enableDesktopWindowingBackNavigation(); + } + + + + public static boolean enableDesktopWindowingEnterTransitionBugfix() { + + return FEATURE_FLAGS.enableDesktopWindowingEnterTransitionBugfix(); + } + + + + public static boolean enableDesktopWindowingEnterTransitions() { + + return FEATURE_FLAGS.enableDesktopWindowingEnterTransitions(); + } + + + + public static boolean enableDesktopWindowingExitByMinimizeTransitionBugfix() { + + return FEATURE_FLAGS.enableDesktopWindowingExitByMinimizeTransitionBugfix(); + } + + + + public static boolean enableDesktopWindowingExitTransitions() { + + return FEATURE_FLAGS.enableDesktopWindowingExitTransitions(); + } + + + + public static boolean enableDesktopWindowingExitTransitionsBugfix() { + + return FEATURE_FLAGS.enableDesktopWindowingExitTransitionsBugfix(); + } + + + + public static boolean enableDesktopWindowingHsum() { + + return FEATURE_FLAGS.enableDesktopWindowingHsum(); + } + + + public static boolean enableDesktopWindowingImmersiveHandleHiding() { + return FEATURE_FLAGS.enableDesktopWindowingImmersiveHandleHiding(); } - + + + public static boolean enableDesktopWindowingModalsPolicy() { + return FEATURE_FLAGS.enableDesktopWindowingModalsPolicy(); } - + + + public static boolean enableDesktopWindowingMode() { + return FEATURE_FLAGS.enableDesktopWindowingMode(); } - + + + + public static boolean enableDesktopWindowingMultiInstanceFeatures() { + + return FEATURE_FLAGS.enableDesktopWindowingMultiInstanceFeatures(); + } + + + + public static boolean enableDesktopWindowingPersistence() { + + return FEATURE_FLAGS.enableDesktopWindowingPersistence(); + } + + + + public static boolean enableDesktopWindowingPip() { + + return FEATURE_FLAGS.enableDesktopWindowingPip(); + } + + + public static boolean enableDesktopWindowingQuickSwitch() { + return FEATURE_FLAGS.enableDesktopWindowingQuickSwitch(); } - - public static boolean enableDesktopWindowingScvhCache() { - return FEATURE_FLAGS.enableDesktopWindowingScvhCache(); + + + + public static boolean enableDesktopWindowingScvhCacheBugFix() { + + return FEATURE_FLAGS.enableDesktopWindowingScvhCacheBugFix(); } - + + + public static boolean enableDesktopWindowingSizeConstraints() { + return FEATURE_FLAGS.enableDesktopWindowingSizeConstraints(); } - + + + public static boolean enableDesktopWindowingTaskLimit() { + return FEATURE_FLAGS.enableDesktopWindowingTaskLimit(); } - + + + public static boolean enableDesktopWindowingTaskbarRunningApps() { + return FEATURE_FLAGS.enableDesktopWindowingTaskbarRunningApps(); } - + + + + public static boolean enableDesktopWindowingTransitions() { + + return FEATURE_FLAGS.enableDesktopWindowingTransitions(); + } + + + public static boolean enableDesktopWindowingWallpaperActivity() { + return FEATURE_FLAGS.enableDesktopWindowingWallpaperActivity(); } - - public static boolean enableScaledResizing() { - return FEATURE_FLAGS.enableScaledResizing(); + + + + public static boolean enableDeviceStateAutoRotateSettingLogging() { + + return FEATURE_FLAGS.enableDeviceStateAutoRotateSettingLogging(); } - + + + + public static boolean enableDeviceStateAutoRotateSettingRefactor() { + + return FEATURE_FLAGS.enableDeviceStateAutoRotateSettingRefactor(); + } + + + + public static boolean enableDisplayDisconnectInteraction() { + + return FEATURE_FLAGS.enableDisplayDisconnectInteraction(); + } + + + + public static boolean enableDisplayFocusInShellTransitions() { + + return FEATURE_FLAGS.enableDisplayFocusInShellTransitions(); + } + + + + public static boolean enableDisplayReconnectInteraction() { + + return FEATURE_FLAGS.enableDisplayReconnectInteraction(); + } + + + + public static boolean enableDisplayWindowingModeSwitching() { + + return FEATURE_FLAGS.enableDisplayWindowingModeSwitching(); + } + + + + public static boolean enableDragResizeSetUpInBgThread() { + + return FEATURE_FLAGS.enableDragResizeSetUpInBgThread(); + } + + + + public static boolean enableDragToDesktopIncomingTransitionsBugfix() { + + return FEATURE_FLAGS.enableDragToDesktopIncomingTransitionsBugfix(); + } + + + + public static boolean enableDragToMaximize() { + + return FEATURE_FLAGS.enableDragToMaximize(); + } + + + + public static boolean enableDynamicRadiusComputationBugfix() { + + return FEATURE_FLAGS.enableDynamicRadiusComputationBugfix(); + } + + + + public static boolean enableFullScreenWindowOnRemovingSplitScreenStageBugfix() { + + return FEATURE_FLAGS.enableFullScreenWindowOnRemovingSplitScreenStageBugfix(); + } + + + + public static boolean enableFullyImmersiveInDesktop() { + + return FEATURE_FLAGS.enableFullyImmersiveInDesktop(); + } + + + + public static boolean enableHandleInputFix() { + + return FEATURE_FLAGS.enableHandleInputFix(); + } + + + + public static boolean enableHoldToDragAppHandle() { + + return FEATURE_FLAGS.enableHoldToDragAppHandle(); + } + + + + public static boolean enableInputLayerTransitionFix() { + + return FEATURE_FLAGS.enableInputLayerTransitionFix(); + } + + + + public static boolean enableMinimizeButton() { + + return FEATURE_FLAGS.enableMinimizeButton(); + } + + + + public static boolean enableModalsFullscreenWithPermission() { + + return FEATURE_FLAGS.enableModalsFullscreenWithPermission(); + } + + + + public static boolean enableMoveToNextDisplayShortcut() { + + return FEATURE_FLAGS.enableMoveToNextDisplayShortcut(); + } + + + + public static boolean enableMultiDisplaySplit() { + + return FEATURE_FLAGS.enableMultiDisplaySplit(); + } + + + + public static boolean enableMultidisplayTrackpadBackGesture() { + + return FEATURE_FLAGS.enableMultidisplayTrackpadBackGesture(); + } + + + + public static boolean enableMultipleDesktopsBackend() { + + return FEATURE_FLAGS.enableMultipleDesktopsBackend(); + } + + + + public static boolean enableMultipleDesktopsFrontend() { + + return FEATURE_FLAGS.enableMultipleDesktopsFrontend(); + } + + + + public static boolean enableNonDefaultDisplaySplit() { + + return FEATURE_FLAGS.enableNonDefaultDisplaySplit(); + } + + + + public static boolean enableOpaqueBackgroundForTransparentWindows() { + + return FEATURE_FLAGS.enableOpaqueBackgroundForTransparentWindows(); + } + + + + public static boolean enablePerDisplayDesktopWallpaperActivity() { + + return FEATURE_FLAGS.enablePerDisplayDesktopWallpaperActivity(); + } + + + + public static boolean enablePerDisplayPackageContextCacheInStatusbarNotif() { + + return FEATURE_FLAGS.enablePerDisplayPackageContextCacheInStatusbarNotif(); + } + + + + public static boolean enablePersistingDisplaySizeForConnectedDisplays() { + + return FEATURE_FLAGS.enablePersistingDisplaySizeForConnectedDisplays(); + } + + + + public static boolean enablePresentationForConnectedDisplays() { + + return FEATURE_FLAGS.enablePresentationForConnectedDisplays(); + } + + + + public static boolean enableProjectedDisplayDesktopMode() { + + return FEATURE_FLAGS.enableProjectedDisplayDesktopMode(); + } + + + + public static boolean enableQuickswitchDesktopSplitBugfix() { + + return FEATURE_FLAGS.enableQuickswitchDesktopSplitBugfix(); + } + + + + public static boolean enableRequestFullscreenBugfix() { + + return FEATURE_FLAGS.enableRequestFullscreenBugfix(); + } + + + + public static boolean enableResizingMetrics() { + + return FEATURE_FLAGS.enableResizingMetrics(); + } + + + + public static boolean enableRestartMenuForConnectedDisplays() { + + return FEATURE_FLAGS.enableRestartMenuForConnectedDisplays(); + } + + + + public static boolean enableRestoreToPreviousSizeFromDesktopImmersive() { + + return FEATURE_FLAGS.enableRestoreToPreviousSizeFromDesktopImmersive(); + } + + + + public static boolean enableShellInitialBoundsRegressionBugFix() { + + return FEATURE_FLAGS.enableShellInitialBoundsRegressionBugFix(); + } + + + + public static boolean enableSizeCompatModeImprovementsForConnectedDisplays() { + + return FEATURE_FLAGS.enableSizeCompatModeImprovementsForConnectedDisplays(); + } + + + + public static boolean enableStartLaunchTransitionFromTaskbarBugfix() { + + return FEATURE_FLAGS.enableStartLaunchTransitionFromTaskbarBugfix(); + } + + + + public static boolean enableTaskResizingKeyboardShortcuts() { + + return FEATURE_FLAGS.enableTaskResizingKeyboardShortcuts(); + } + + + public static boolean enableTaskStackObserverInShell() { + return FEATURE_FLAGS.enableTaskStackObserverInShell(); } - + + + + public static boolean enableTaskbarConnectedDisplays() { + + return FEATURE_FLAGS.enableTaskbarConnectedDisplays(); + } + + + + public static boolean enableTaskbarOverflow() { + + return FEATURE_FLAGS.enableTaskbarOverflow(); + } + + + + public static boolean enableTaskbarRecentsLayoutTransition() { + + return FEATURE_FLAGS.enableTaskbarRecentsLayoutTransition(); + } + + + public static boolean enableThemedAppHeaders() { + return FEATURE_FLAGS.enableThemedAppHeaders(); } - + + + + public static boolean enableTileResizing() { + + return FEATURE_FLAGS.enableTileResizing(); + } + + + + public static boolean enableTopVisibleRootTaskPerUserTracking() { + + return FEATURE_FLAGS.enableTopVisibleRootTaskPerUserTracking(); + } + + + + public static boolean enableVisualIndicatorInTransitionBugfix() { + + return FEATURE_FLAGS.enableVisualIndicatorInTransitionBugfix(); + } + + + + public static boolean enableWindowContextResourcesUpdateOnConfigChange() { + + return FEATURE_FLAGS.enableWindowContextResourcesUpdateOnConfigChange(); + } + + + public static boolean enableWindowingDynamicInitialBounds() { + return FEATURE_FLAGS.enableWindowingDynamicInitialBounds(); } - + + + public static boolean enableWindowingEdgeDragResize() { + return FEATURE_FLAGS.enableWindowingEdgeDragResize(); } - - public static boolean enableWmExtensionsForAllFlag() { - return FEATURE_FLAGS.enableWmExtensionsForAllFlag(); + + + + public static boolean enableWindowingScaledResizing() { + + return FEATURE_FLAGS.enableWindowingScaledResizing(); } - + + + + public static boolean enableWindowingTransitionHandlersObservers() { + + return FEATURE_FLAGS.enableWindowingTransitionHandlersObservers(); + } + + + public static boolean enforceEdgeToEdge() { + return FEATURE_FLAGS.enforceEdgeToEdge(); } - + + + + public static boolean ensureKeyguardDoesTransitionStarting() { + + return FEATURE_FLAGS.ensureKeyguardDoesTransitionStarting(); + } + + + public static boolean ensureWallpaperInTransitions() { + return FEATURE_FLAGS.ensureWallpaperInTransitions(); } - - public static boolean explicitRefreshRateHints() { - return FEATURE_FLAGS.explicitRefreshRateHints(); + + + + public static boolean ensureWallpaperInWearTransitions() { + + return FEATURE_FLAGS.ensureWallpaperInWearTransitions(); } - + + + + public static boolean enterDesktopByDefaultOnFreeformDisplays() { + + return FEATURE_FLAGS.enterDesktopByDefaultOnFreeformDisplays(); + } + + + + public static boolean excludeCaptionFromAppBounds() { + + return FEATURE_FLAGS.excludeCaptionFromAppBounds(); + } + + + + public static boolean excludeDrawingAppThemeSnapshotFromLock() { + + return FEATURE_FLAGS.excludeDrawingAppThemeSnapshotFromLock(); + } + + + + public static boolean excludeTaskFromRecents() { + + return FEATURE_FLAGS.excludeTaskFromRecents(); + } + + + public static boolean fifoPriorityForMajorUiProcesses() { + return FEATURE_FLAGS.fifoPriorityForMajorUiProcesses(); } - - public static boolean fixNoContainerUpdateWithoutResize() { - return FEATURE_FLAGS.fixNoContainerUpdateWithoutResize(); + + + + public static boolean fixHideOverlayApi() { + + return FEATURE_FLAGS.fixHideOverlayApi(); } - - public static boolean fixPipRestoreToOverlay() { - return FEATURE_FLAGS.fixPipRestoreToOverlay(); + + + + public static boolean fixLayoutExistingTask() { + + return FEATURE_FLAGS.fixLayoutExistingTask(); } - - public static boolean fullscreenDimFlag() { - return FEATURE_FLAGS.fullscreenDimFlag(); + + + + public static boolean fixViewRootCallTrace() { + + return FEATURE_FLAGS.fixViewRootCallTrace(); } - + + + + public static boolean forceCloseTopTransparentFullscreenTask() { + + return FEATURE_FLAGS.forceCloseTopTransparentFullscreenTask(); + } + + + + public static boolean formFactorBasedDesktopFirstSwitch() { + + return FEATURE_FLAGS.formFactorBasedDesktopFirstSwitch(); + } + + + public static boolean getDimmerOnClosing() { + return FEATURE_FLAGS.getDimmerOnClosing(); } - - public static boolean immersiveAppRepositioning() { - return FEATURE_FLAGS.immersiveAppRepositioning(); + + + + public static boolean ignoreAspectRatioRestrictionsForResizeableFreeformActivities() { + + return FEATURE_FLAGS.ignoreAspectRatioRestrictionsForResizeableFreeformActivities(); } - - public static boolean insetsControlChangedItem() { - return FEATURE_FLAGS.insetsControlChangedItem(); + + + + public static boolean ignoreCornerRadiusAndShadows() { + + return FEATURE_FLAGS.ignoreCornerRadiusAndShadows(); } - - public static boolean insetsControlSeq() { - return FEATURE_FLAGS.insetsControlSeq(); + + + + public static boolean includeTopTransparentFullscreenTaskInDesktopHeuristic() { + + return FEATURE_FLAGS.includeTopTransparentFullscreenTaskInDesktopHeuristic(); } - + + + + public static boolean inheritTaskBoundsForTrampolineTaskLaunches() { + + return FEATURE_FLAGS.inheritTaskBoundsForTrampolineTaskLaunches(); + } + + + public static boolean insetsDecoupledConfiguration() { + return FEATURE_FLAGS.insetsDecoupledConfiguration(); } - - public static boolean introduceSmootherDimmer() { - return FEATURE_FLAGS.introduceSmootherDimmer(); + + + + public static boolean jankApi() { + + return FEATURE_FLAGS.jankApi(); } - - public static boolean keyguardAppearTransition() { - return FEATURE_FLAGS.keyguardAppearTransition(); + + + + public static boolean keepAppWindowHideWhileLocked() { + + return FEATURE_FLAGS.keepAppWindowHideWhileLocked(); } - + + + + public static boolean keyboardShortcutsToSwitchDesks() { + + return FEATURE_FLAGS.keyboardShortcutsToSwitchDesks(); + } + + + + public static boolean keyguardGoingAwayTimeout() { + + return FEATURE_FLAGS.keyguardGoingAwayTimeout(); + } + + + public static boolean letterboxBackgroundWallpaper() { + return FEATURE_FLAGS.letterboxBackgroundWallpaper(); } - + + + public static boolean movableCutoutConfiguration() { + return FEATURE_FLAGS.movableCutoutConfiguration(); } - - public static boolean moveAnimationOptionsToChange() { - return FEATURE_FLAGS.moveAnimationOptionsToChange(); + + + + public static boolean moveToExternalDisplayShortcut() { + + return FEATURE_FLAGS.moveToExternalDisplayShortcut(); } - + + + public static boolean multiCrop() { + return FEATURE_FLAGS.multiCrop(); } - + + + public static boolean navBarTransparentByDefault() { + return FEATURE_FLAGS.navBarTransparentByDefault(); } - + + + + public static boolean nestedTasksWithIndependentBounds() { + + return FEATURE_FLAGS.nestedTasksWithIndependentBounds(); + } + + + public static boolean noConsecutiveVisibilityEvents() { + return FEATURE_FLAGS.noConsecutiveVisibilityEvents(); } - + + + + public static boolean noDuplicateSurfaceDestroyedEvents() { + + return FEATURE_FLAGS.noDuplicateSurfaceDestroyedEvents(); + } + + + public static boolean noVisibilityEventOnDisplayStateChange() { + return FEATURE_FLAGS.noVisibilityEventOnDisplayStateChange(); } - + + + public static boolean offloadColorExtraction() { + return FEATURE_FLAGS.offloadColorExtraction(); } - - public static boolean predictiveBackSystemAnims() { - return FEATURE_FLAGS.predictiveBackSystemAnims(); + + + + public static boolean portWindowSizeAnimation() { + + return FEATURE_FLAGS.portWindowSizeAnimation(); } - + + + + public static boolean predictiveBackDefaultEnableSdk36() { + + return FEATURE_FLAGS.predictiveBackDefaultEnableSdk36(); + } + + + + public static boolean predictiveBackPrioritySystemNavigationObserver() { + + return FEATURE_FLAGS.predictiveBackPrioritySystemNavigationObserver(); + } + + + + public static boolean predictiveBackSwipeEdgeNoneApi() { + + return FEATURE_FLAGS.predictiveBackSwipeEdgeNoneApi(); + } + + + + public static boolean predictiveBackSystemOverrideCallback() { + + return FEATURE_FLAGS.predictiveBackSystemOverrideCallback(); + } + + + + public static boolean predictiveBackThreeButtonNav() { + + return FEATURE_FLAGS.predictiveBackThreeButtonNav(); + } + + + + public static boolean predictiveBackTimestampApi() { + + return FEATURE_FLAGS.predictiveBackTimestampApi(); + } + + + + public static boolean processPriorityPolicyForMultiWindowMode() { + + return FEATURE_FLAGS.processPriorityPolicyForMultiWindowMode(); + } + + + public static boolean rearDisplayDisableForceDesktopSystemDecorations() { + return FEATURE_FLAGS.rearDisplayDisableForceDesktopSystemDecorations(); } - + + + + public static boolean recordTaskSnapshotsBeforeShutdown() { + + return FEATURE_FLAGS.recordTaskSnapshotsBeforeShutdown(); + } + + + + public static boolean reduceChangedExclusionRectsMsgs() { + + return FEATURE_FLAGS.reduceChangedExclusionRectsMsgs(); + } + + + + public static boolean reduceKeyguardTransitions() { + + return FEATURE_FLAGS.reduceKeyguardTransitions(); + } + + + + public static boolean reduceTaskSnapshotMemoryUsage() { + + return FEATURE_FLAGS.reduceTaskSnapshotMemoryUsage(); + } + + + + public static boolean reduceUnnecessaryMeasure() { + + return FEATURE_FLAGS.reduceUnnecessaryMeasure(); + } + + + + public static boolean relativeInsets() { + + return FEATURE_FLAGS.relativeInsets(); + } + + + public static boolean releaseSnapshotAggressively() { + return FEATURE_FLAGS.releaseSnapshotAggressively(); } - - public static boolean removePrepareSurfaceInPlacement() { - return FEATURE_FLAGS.removePrepareSurfaceInPlacement(); + + + + public static boolean releaseUserAspectRatioWm() { + + return FEATURE_FLAGS.releaseUserAspectRatioWm(); } - + + + + public static boolean removeActivityStarterDreamCallback() { + + return FEATURE_FLAGS.removeActivityStarterDreamCallback(); + } + + + + public static boolean removeDeferHidingClient() { + + return FEATURE_FLAGS.removeDeferHidingClient(); + } + + + + public static boolean removeDepartTargetFromMotion() { + + return FEATURE_FLAGS.removeDepartTargetFromMotion(); + } + + + + public static boolean reparentWindowTokenApi() { + + return FEATURE_FLAGS.reparentWindowTokenApi(); + } + + + + public static boolean respectNonTopVisibleFixedOrientation() { + + return FEATURE_FLAGS.respectNonTopVisibleFixedOrientation(); + } + + + + public static boolean respectOrientationChangeForUnresizeable() { + + return FEATURE_FLAGS.respectOrientationChangeForUnresizeable(); + } + + + + public static boolean safeRegionLetterboxing() { + + return FEATURE_FLAGS.safeRegionLetterboxing(); + } + + + + public static boolean safeReleaseSnapshotAggressively() { + + return FEATURE_FLAGS.safeReleaseSnapshotAggressively(); + } + + + + public static boolean schedulingForNotificationShade() { + + return FEATURE_FLAGS.schedulingForNotificationShade(); + } + + + + public static boolean scrambleSnapshotFileName() { + + return FEATURE_FLAGS.scrambleSnapshotFileName(); + } + + + public static boolean screenRecordingCallbacks() { + return FEATURE_FLAGS.screenRecordingCallbacks(); } - + + + + public static boolean scrollingFromLetterbox() { + + return FEATURE_FLAGS.scrollingFromLetterbox(); + } + + + public static boolean sdkDesiredPresentTime() { + return FEATURE_FLAGS.sdkDesiredPresentTime(); } - - public static boolean secureWindowState() { - return FEATURE_FLAGS.secureWindowState(); - } - + + + public static boolean setScPropertiesInClient() { + return FEATURE_FLAGS.setScPropertiesInClient(); } - - public static boolean skipSleepingWhenSwitchingDisplay() { - return FEATURE_FLAGS.skipSleepingWhenSwitchingDisplay(); + + + + public static boolean showAppHandleLargeScreens() { + + return FEATURE_FLAGS.showAppHandleLargeScreens(); } - + + + + public static boolean showDesktopExperienceDevOption() { + + return FEATURE_FLAGS.showDesktopExperienceDevOption(); + } + + + + public static boolean showDesktopWindowingDevOption() { + + return FEATURE_FLAGS.showDesktopWindowingDevOption(); + } + + + + public static boolean showHomeBehindDesktop() { + + return FEATURE_FLAGS.showHomeBehindDesktop(); + } + + + + public static boolean skipCompatUiEducationInDesktopMode() { + + return FEATURE_FLAGS.skipCompatUiEducationInDesktopMode(); + } + + + + public static boolean skipDecorViewRelayoutWhenClosingBugfix() { + + return FEATURE_FLAGS.skipDecorViewRelayoutWhenClosingBugfix(); + } + + + + public static boolean supportWidgetIntentsOnConnectedDisplay() { + + return FEATURE_FLAGS.supportWidgetIntentsOnConnectedDisplay(); + } + + + + public static boolean supportsDragAssistantToMultiwindow() { + + return FEATURE_FLAGS.supportsDragAssistantToMultiwindow(); + } + + + public static boolean supportsMultiInstanceSystemUi() { + return FEATURE_FLAGS.supportsMultiInstanceSystemUi(); } - + + + public static boolean surfaceControlInputReceiver() { + return FEATURE_FLAGS.surfaceControlInputReceiver(); } - + + + public static boolean surfaceTrustedOverlay() { + return FEATURE_FLAGS.surfaceTrustedOverlay(); } - + + + public static boolean syncScreenCapture() { + return FEATURE_FLAGS.syncScreenCapture(); } - + + + + public static boolean systemUiPostAnimationEnd() { + + return FEATURE_FLAGS.systemUiPostAnimationEnd(); + } + + + public static boolean taskFragmentSystemOrganizerFlag() { + return FEATURE_FLAGS.taskFragmentSystemOrganizerFlag(); } - + + + + public static boolean touchPassThroughOptIn() { + + return FEATURE_FLAGS.touchPassThroughOptIn(); + } + + + + public static boolean trackSystemUiContextBeforeWms() { + + return FEATURE_FLAGS.trackSystemUiContextBeforeWms(); + } + + + public static boolean transitReadyTracking() { + return FEATURE_FLAGS.transitReadyTracking(); } - + + + + public static boolean transitTrackerPlumbing() { + + return FEATURE_FLAGS.transitTrackerPlumbing(); + } + + + public static boolean trustedPresentationListenerForWindow() { + return FEATURE_FLAGS.trustedPresentationListenerForWindow(); } - + + + + public static boolean unifyBackNavigationTransition() { + + return FEATURE_FLAGS.unifyBackNavigationTransition(); + } + + + + public static boolean universalResizableByDefault() { + + return FEATURE_FLAGS.universalResizableByDefault(); + } + + + public static boolean untrustedEmbeddingAnyAppPermission() { + return FEATURE_FLAGS.untrustedEmbeddingAnyAppPermission(); } - + + + public static boolean untrustedEmbeddingStateSharing() { + return FEATURE_FLAGS.untrustedEmbeddingStateSharing(); } - + + + + public static boolean updateDimsWhenWindowShown() { + + return FEATURE_FLAGS.updateDimsWhenWindowShown(); + } + + + + public static boolean useCachedInsetsForDisplaySwitch() { + + return FEATURE_FLAGS.useCachedInsetsForDisplaySwitch(); + } + + + + public static boolean useRtFrameCallbackForSplashScreenTransfer() { + + return FEATURE_FLAGS.useRtFrameCallbackForSplashScreenTransfer(); + } + + + + public static boolean useTasksDimOnly() { + + return FEATURE_FLAGS.useTasksDimOnly(); + } + + + + public static boolean useVisibleRequestedForProcessTracker() { + + return FEATURE_FLAGS.useVisibleRequestedForProcessTracker(); + } + + + public static boolean useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds() { + return FEATURE_FLAGS.useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds(); } - - public static boolean userMinAspectRatioAppDefault() { - return FEATURE_FLAGS.userMinAspectRatioAppDefault(); + + + + public static boolean vdmForceAppUniversalResizableApi() { + + return FEATURE_FLAGS.vdmForceAppUniversalResizableApi(); } - - public static boolean waitForTransitionOnDisplaySwitch() { - return FEATURE_FLAGS.waitForTransitionOnDisplaySwitch(); - } - + + + public static boolean wallpaperOffsetAsync() { + return FEATURE_FLAGS.wallpaperOffsetAsync(); } - - public static boolean windowSessionRelayoutInfo() { - return FEATURE_FLAGS.windowSessionRelayoutInfo(); - } - - public static boolean windowTokenConfigThreadSafe() { - return FEATURE_FLAGS.windowTokenConfigThreadSafe(); + + + + public static boolean wlinfoOncreate() { + + return FEATURE_FLAGS.wlinfoOncreate(); } private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); diff --git a/flags/src/com/android/wm/shell/CustomFeatureFlags.java b/flags/src/com/android/wm/shell/CustomFeatureFlags.java index e8a55c4591..ce1e21d693 100644 --- a/flags/src/com/android/wm/shell/CustomFeatureFlags.java +++ b/flags/src/com/android/wm/shell/CustomFeatureFlags.java @@ -1,13 +1,13 @@ package com.android.wm.shell; // TODO(b/303773055): Remove the annotation after access issue is resolved. + import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Predicate; - /** @hide */ public class CustomFeatureFlags implements FeatureFlags { @@ -18,124 +18,216 @@ public class CustomFeatureFlags implements FeatureFlags { } @Override - public boolean animateBubbleSizeChange() { - return getValue(Flags.FLAG_ANIMATE_BUBBLE_SIZE_CHANGE, - FeatureFlags::animateBubbleSizeChange); + public boolean bubbleViewInfoExecutors() { + return getValue(Flags.FLAG_BUBBLE_VIEW_INFO_EXECUTORS, + FeatureFlags::bubbleViewInfoExecutors); } @Override - - public boolean enableAppPairs() { - return getValue(Flags.FLAG_ENABLE_APP_PAIRS, - FeatureFlags::enableAppPairs); + + public boolean enableAutoTaskStackController() { + return getValue(Flags.FLAG_ENABLE_AUTO_TASK_STACK_CONTROLLER, + FeatureFlags::enableAutoTaskStackController); } @Override - + public boolean enableBubbleAnything() { return getValue(Flags.FLAG_ENABLE_BUBBLE_ANYTHING, - FeatureFlags::enableBubbleAnything); + FeatureFlags::enableBubbleAnything); } @Override - + public boolean enableBubbleBar() { return getValue(Flags.FLAG_ENABLE_BUBBLE_BAR, - FeatureFlags::enableBubbleBar); + FeatureFlags::enableBubbleBar); } @Override - + + public boolean enableBubbleBarOnPhones() { + return getValue(Flags.FLAG_ENABLE_BUBBLE_BAR_ON_PHONES, + FeatureFlags::enableBubbleBarOnPhones); + } + + @Override + public boolean enableBubbleStashing() { return getValue(Flags.FLAG_ENABLE_BUBBLE_STASHING, - FeatureFlags::enableBubbleStashing); + FeatureFlags::enableBubbleStashing); } @Override - + + public boolean enableBubbleTaskViewListener() { + return getValue(Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER, + FeatureFlags::enableBubbleTaskViewListener); + } + + @Override + + public boolean enableBubbleToFullscreen() { + return getValue(Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + FeatureFlags::enableBubbleToFullscreen); + } + + @Override + public boolean enableBubblesLongPressNavHandle() { return getValue(Flags.FLAG_ENABLE_BUBBLES_LONG_PRESS_NAV_HANDLE, - FeatureFlags::enableBubblesLongPressNavHandle); + FeatureFlags::enableBubblesLongPressNavHandle); } @Override - - public boolean enableLeftRightSplitInPortrait() { - return getValue(Flags.FLAG_ENABLE_LEFT_RIGHT_SPLIT_IN_PORTRAIT, - FeatureFlags::enableLeftRightSplitInPortrait); + + public boolean enableCreateAnyBubble() { + return getValue(Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + FeatureFlags::enableCreateAnyBubble); } @Override - + + public boolean enableDynamicInsetsForAppLaunch() { + return getValue(Flags.FLAG_ENABLE_DYNAMIC_INSETS_FOR_APP_LAUNCH, + FeatureFlags::enableDynamicInsetsForAppLaunch); + } + + @Override + + public boolean enableFlexibleSplit() { + return getValue(Flags.FLAG_ENABLE_FLEXIBLE_SPLIT, + FeatureFlags::enableFlexibleSplit); + } + + @Override + + public boolean enableFlexibleTwoAppSplit() { + return getValue(Flags.FLAG_ENABLE_FLEXIBLE_TWO_APP_SPLIT, + FeatureFlags::enableFlexibleTwoAppSplit); + } + + @Override + + public boolean enableGsf() { + return getValue(Flags.FLAG_ENABLE_GSF, + FeatureFlags::enableGsf); + } + + @Override + + public boolean enableMagneticSplitDivider() { + return getValue(Flags.FLAG_ENABLE_MAGNETIC_SPLIT_DIVIDER, + FeatureFlags::enableMagneticSplitDivider); + } + + @Override + public boolean enableNewBubbleAnimations() { return getValue(Flags.FLAG_ENABLE_NEW_BUBBLE_ANIMATIONS, - FeatureFlags::enableNewBubbleAnimations); + FeatureFlags::enableNewBubbleAnimations); } @Override - + public boolean enableOptionalBubbleOverflow() { return getValue(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW, - FeatureFlags::enableOptionalBubbleOverflow); + FeatureFlags::enableOptionalBubbleOverflow); } @Override - - public boolean enablePip2Implementation() { - return getValue(Flags.FLAG_ENABLE_PIP2_IMPLEMENTATION, - FeatureFlags::enablePip2Implementation); + + public boolean enablePip2() { + return getValue(Flags.FLAG_ENABLE_PIP2, + FeatureFlags::enablePip2); } @Override - + public boolean enablePipUmoExperience() { return getValue(Flags.FLAG_ENABLE_PIP_UMO_EXPERIENCE, - FeatureFlags::enablePipUmoExperience); + FeatureFlags::enablePipUmoExperience); } @Override - + + public boolean enableRecentsBookendTransition() { + return getValue(Flags.FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION, + FeatureFlags::enableRecentsBookendTransition); + } + + @Override + public boolean enableRetrievableBubbles() { return getValue(Flags.FLAG_ENABLE_RETRIEVABLE_BUBBLES, - FeatureFlags::enableRetrievableBubbles); + FeatureFlags::enableRetrievableBubbles); } @Override - - public boolean enableSplitContextual() { - return getValue(Flags.FLAG_ENABLE_SPLIT_CONTEXTUAL, - FeatureFlags::enableSplitContextual); + + public boolean enableShellTopTaskTracking() { + return getValue(Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING, + FeatureFlags::enableShellTopTaskTracking); } @Override - + + public boolean enableTaskViewControllerCleanup() { + return getValue(Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP, + FeatureFlags::enableTaskViewControllerCleanup); + } + + @Override + public boolean enableTaskbarNavbarUnification() { return getValue(Flags.FLAG_ENABLE_TASKBAR_NAVBAR_UNIFICATION, - FeatureFlags::enableTaskbarNavbarUnification); + FeatureFlags::enableTaskbarNavbarUnification); } @Override - + + public boolean enableTaskbarOnPhones() { + return getValue(Flags.FLAG_ENABLE_TASKBAR_ON_PHONES, + FeatureFlags::enableTaskbarOnPhones); + } + + @Override + public boolean enableTinyTaskbar() { return getValue(Flags.FLAG_ENABLE_TINY_TASKBAR, - FeatureFlags::enableTinyTaskbar); + FeatureFlags::enableTinyTaskbar); } @Override - + + public boolean fixMissingUserChangeCallbacks() { + return getValue(Flags.FLAG_FIX_MISSING_USER_CHANGE_CALLBACKS, + FeatureFlags::fixMissingUserChangeCallbacks); + } + + @Override + public boolean onlyReuseBubbledTaskWhenLaunchedFromBubble() { return getValue(Flags.FLAG_ONLY_REUSE_BUBBLED_TASK_WHEN_LAUNCHED_FROM_BUBBLE, - FeatureFlags::onlyReuseBubbledTaskWhenLaunchedFromBubble); + FeatureFlags::onlyReuseBubbledTaskWhenLaunchedFromBubble); + } + + @Override + + public boolean taskViewRepository() { + return getValue(Flags.FLAG_TASK_VIEW_REPOSITORY, + FeatureFlags::taskViewRepository); } public boolean isFlagReadOnlyOptimized(String flagName) { if (mReadOnlyFlagsSet.contains(flagName) && - isOptimizationEnabled()) { - return true; + isOptimizationEnabled()) { + return true; } return false; } + private boolean isOptimizationEnabled() { return false; } @@ -146,29 +238,70 @@ public class CustomFeatureFlags implements FeatureFlags { public List getFlagNames() { return Arrays.asList( - Flags.FLAG_ANIMATE_BUBBLE_SIZE_CHANGE, - Flags.FLAG_ENABLE_APP_PAIRS, - Flags.FLAG_ENABLE_BUBBLE_ANYTHING, - Flags.FLAG_ENABLE_BUBBLE_BAR, - Flags.FLAG_ENABLE_BUBBLE_STASHING, - Flags.FLAG_ENABLE_BUBBLES_LONG_PRESS_NAV_HANDLE, - Flags.FLAG_ENABLE_LEFT_RIGHT_SPLIT_IN_PORTRAIT, - Flags.FLAG_ENABLE_NEW_BUBBLE_ANIMATIONS, - Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW, - Flags.FLAG_ENABLE_PIP2_IMPLEMENTATION, - Flags.FLAG_ENABLE_PIP_UMO_EXPERIENCE, - Flags.FLAG_ENABLE_RETRIEVABLE_BUBBLES, - Flags.FLAG_ENABLE_SPLIT_CONTEXTUAL, - Flags.FLAG_ENABLE_TASKBAR_NAVBAR_UNIFICATION, - Flags.FLAG_ENABLE_TINY_TASKBAR, - Flags.FLAG_ONLY_REUSE_BUBBLED_TASK_WHEN_LAUNCHED_FROM_BUBBLE + Flags.FLAG_BUBBLE_VIEW_INFO_EXECUTORS, + Flags.FLAG_ENABLE_AUTO_TASK_STACK_CONTROLLER, + Flags.FLAG_ENABLE_BUBBLE_ANYTHING, + Flags.FLAG_ENABLE_BUBBLE_BAR, + Flags.FLAG_ENABLE_BUBBLE_BAR_ON_PHONES, + Flags.FLAG_ENABLE_BUBBLE_STASHING, + Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER, + Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + Flags.FLAG_ENABLE_BUBBLES_LONG_PRESS_NAV_HANDLE, + Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + Flags.FLAG_ENABLE_DYNAMIC_INSETS_FOR_APP_LAUNCH, + Flags.FLAG_ENABLE_FLEXIBLE_SPLIT, + Flags.FLAG_ENABLE_FLEXIBLE_TWO_APP_SPLIT, + Flags.FLAG_ENABLE_GSF, + Flags.FLAG_ENABLE_MAGNETIC_SPLIT_DIVIDER, + Flags.FLAG_ENABLE_NEW_BUBBLE_ANIMATIONS, + Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW, + Flags.FLAG_ENABLE_PIP2, + Flags.FLAG_ENABLE_PIP_UMO_EXPERIENCE, + Flags.FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION, + Flags.FLAG_ENABLE_RETRIEVABLE_BUBBLES, + Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING, + Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP, + Flags.FLAG_ENABLE_TASKBAR_NAVBAR_UNIFICATION, + Flags.FLAG_ENABLE_TASKBAR_ON_PHONES, + Flags.FLAG_ENABLE_TINY_TASKBAR, + Flags.FLAG_FIX_MISSING_USER_CHANGE_CALLBACKS, + Flags.FLAG_ONLY_REUSE_BUBBLED_TASK_WHEN_LAUNCHED_FROM_BUBBLE, + Flags.FLAG_TASK_VIEW_REPOSITORY ); } private Set mReadOnlyFlagsSet = new HashSet<>( - Arrays.asList( - Flags.FLAG_ENABLE_PIP2_IMPLEMENTATION, - "" - ) + Arrays.asList( + Flags.FLAG_BUBBLE_VIEW_INFO_EXECUTORS, + Flags.FLAG_ENABLE_AUTO_TASK_STACK_CONTROLLER, + Flags.FLAG_ENABLE_BUBBLE_ANYTHING, + Flags.FLAG_ENABLE_BUBBLE_BAR, + Flags.FLAG_ENABLE_BUBBLE_BAR_ON_PHONES, + Flags.FLAG_ENABLE_BUBBLE_STASHING, + Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER, + Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + Flags.FLAG_ENABLE_BUBBLES_LONG_PRESS_NAV_HANDLE, + Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + Flags.FLAG_ENABLE_DYNAMIC_INSETS_FOR_APP_LAUNCH, + Flags.FLAG_ENABLE_FLEXIBLE_SPLIT, + Flags.FLAG_ENABLE_FLEXIBLE_TWO_APP_SPLIT, + Flags.FLAG_ENABLE_GSF, + Flags.FLAG_ENABLE_MAGNETIC_SPLIT_DIVIDER, + Flags.FLAG_ENABLE_NEW_BUBBLE_ANIMATIONS, + Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW, + Flags.FLAG_ENABLE_PIP2, + Flags.FLAG_ENABLE_PIP_UMO_EXPERIENCE, + Flags.FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION, + Flags.FLAG_ENABLE_RETRIEVABLE_BUBBLES, + Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING, + Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP, + Flags.FLAG_ENABLE_TASKBAR_NAVBAR_UNIFICATION, + Flags.FLAG_ENABLE_TASKBAR_ON_PHONES, + Flags.FLAG_ENABLE_TINY_TASKBAR, + Flags.FLAG_FIX_MISSING_USER_CHANGE_CALLBACKS, + Flags.FLAG_ONLY_REUSE_BUBBLED_TASK_WHEN_LAUNCHED_FROM_BUBBLE, + Flags.FLAG_TASK_VIEW_REPOSITORY, + "" + ) ); } diff --git a/flags/src/com/android/wm/shell/FakeFeatureFlagsImpl.java b/flags/src/com/android/wm/shell/FakeFeatureFlagsImpl.java index c4594f283f..ade311576c 100644 --- a/flags/src/com/android/wm/shell/FakeFeatureFlagsImpl.java +++ b/flags/src/com/android/wm/shell/FakeFeatureFlagsImpl.java @@ -3,7 +3,6 @@ package com.android.wm.shell; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; - /** @hide */ public class FakeFeatureFlagsImpl extends CustomFeatureFlags { private final Map mFlagMap = new HashMap<>(); diff --git a/flags/src/com/android/wm/shell/FeatureFlags.java b/flags/src/com/android/wm/shell/FeatureFlags.java index 5c5ba5bcc0..5441eaed38 100644 --- a/flags/src/com/android/wm/shell/FeatureFlags.java +++ b/flags/src/com/android/wm/shell/FeatureFlags.java @@ -1,53 +1,123 @@ package com.android.wm.shell; // TODO(b/303773055): Remove the annotation after access issue is resolved. + /** @hide */ public interface FeatureFlags { - - boolean animateBubbleSizeChange(); - - - boolean enableAppPairs(); - - + + + boolean bubbleViewInfoExecutors(); + + + + boolean enableAutoTaskStackController(); + + + boolean enableBubbleAnything(); - - + + + boolean enableBubbleBar(); - - + + + + boolean enableBubbleBarOnPhones(); + + + boolean enableBubbleStashing(); - - + + + + boolean enableBubbleTaskViewListener(); + + + + boolean enableBubbleToFullscreen(); + + + boolean enableBubblesLongPressNavHandle(); - - - boolean enableLeftRightSplitInPortrait(); - - + + + + boolean enableCreateAnyBubble(); + + + + boolean enableDynamicInsetsForAppLaunch(); + + + + boolean enableFlexibleSplit(); + + + + boolean enableFlexibleTwoAppSplit(); + + + + boolean enableGsf(); + + + + boolean enableMagneticSplitDivider(); + + + boolean enableNewBubbleAnimations(); - - + + + boolean enableOptionalBubbleOverflow(); - - boolean enablePip2Implementation(); - - + + + + boolean enablePip2(); + + + boolean enablePipUmoExperience(); - - + + + + boolean enableRecentsBookendTransition(); + + + boolean enableRetrievableBubbles(); - - - boolean enableSplitContextual(); - - + + + + boolean enableShellTopTaskTracking(); + + + + boolean enableTaskViewControllerCleanup(); + + + boolean enableTaskbarNavbarUnification(); - - + + + + boolean enableTaskbarOnPhones(); + + + boolean enableTinyTaskbar(); - - + + + + boolean fixMissingUserChangeCallbacks(); + + + boolean onlyReuseBubbledTaskWhenLaunchedFromBubble(); + + + + boolean taskViewRepository(); } diff --git a/flags/src/com/android/wm/shell/FeatureFlagsImpl.java b/flags/src/com/android/wm/shell/FeatureFlagsImpl.java index a5bba4b5ee..8620fce733 100644 --- a/flags/src/com/android/wm/shell/FeatureFlagsImpl.java +++ b/flags/src/com/android/wm/shell/FeatureFlagsImpl.java @@ -1,394 +1,209 @@ package com.android.wm.shell; // TODO(b/303773055): Remove the annotation after access issue is resolved. -import com.android.quickstep.util.DeviceConfigHelper; - -import java.nio.file.Files; -import java.nio.file.Paths; /** @hide */ public final class FeatureFlagsImpl implements FeatureFlags { - private static final boolean isReadFromNew = Files.exists(Paths.get("/metadata/aconfig/boot/enable_only_new_storage")); - private static volatile boolean isCached = false; - private static volatile boolean multitasking_is_cached = false; - private static boolean animateBubbleSizeChange = false; - private static boolean enableAppPairs = true; - private static boolean enableBubbleAnything = false; - private static boolean enableBubbleBar = false; - private static boolean enableBubbleStashing = false; - private static boolean enableBubblesLongPressNavHandle = false; - private static boolean enableLeftRightSplitInPortrait = true; - private static boolean enableNewBubbleAnimations = false; - private static boolean enableOptionalBubbleOverflow = true; - private static boolean enablePipUmoExperience = false; - private static boolean enableRetrievableBubbles = false; - private static boolean enableSplitContextual = true; - private static boolean enableTaskbarNavbarUnification = true; - private static boolean enableTinyTaskbar = false; - private static boolean onlyReuseBubbledTaskWhenLaunchedFromBubble = false; + @Override - private void init() { - boolean foundPackage = true; - - animateBubbleSizeChange = foundPackage; - - - enableAppPairs = foundPackage; - - - enableBubbleAnything = foundPackage; - - - enableBubbleBar = foundPackage; - - - enableBubbleStashing = foundPackage; - - - enableBubblesLongPressNavHandle = foundPackage ; - - - enableLeftRightSplitInPortrait = foundPackage; - - - enableNewBubbleAnimations = foundPackage; - - - enableOptionalBubbleOverflow = foundPackage; - - - - enablePipUmoExperience = foundPackage; - - - enableRetrievableBubbles = foundPackage; - - - enableSplitContextual = foundPackage; - - - enableTaskbarNavbarUnification = foundPackage; - - - enableTinyTaskbar = foundPackage; - - - onlyReuseBubbledTaskWhenLaunchedFromBubble = foundPackage ; - - isCached = true; - } - - - - - private void load_overrides_multitasking() { - try { - var properties = DeviceConfigHelper.Companion.getPrefs(); - animateBubbleSizeChange = - properties.getBoolean(Flags.FLAG_ANIMATE_BUBBLE_SIZE_CHANGE, false); - enableAppPairs = - properties.getBoolean(Flags.FLAG_ENABLE_APP_PAIRS, true); - enableBubbleAnything = - properties.getBoolean(Flags.FLAG_ENABLE_BUBBLE_ANYTHING, false); - enableBubbleBar = - properties.getBoolean(Flags.FLAG_ENABLE_BUBBLE_BAR, false); - enableBubbleStashing = - properties.getBoolean(Flags.FLAG_ENABLE_BUBBLE_STASHING, false); - enableBubblesLongPressNavHandle = - properties.getBoolean(Flags.FLAG_ENABLE_BUBBLES_LONG_PRESS_NAV_HANDLE, false); - enableLeftRightSplitInPortrait = - properties.getBoolean(Flags.FLAG_ENABLE_LEFT_RIGHT_SPLIT_IN_PORTRAIT, true); - enableNewBubbleAnimations = - properties.getBoolean(Flags.FLAG_ENABLE_NEW_BUBBLE_ANIMATIONS, false); - enableOptionalBubbleOverflow = - properties.getBoolean(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW, true); - enablePipUmoExperience = - properties.getBoolean(Flags.FLAG_ENABLE_PIP_UMO_EXPERIENCE, false); - enableRetrievableBubbles = - properties.getBoolean(Flags.FLAG_ENABLE_RETRIEVABLE_BUBBLES, false); - enableSplitContextual = - properties.getBoolean(Flags.FLAG_ENABLE_SPLIT_CONTEXTUAL, true); - enableTaskbarNavbarUnification = - properties.getBoolean(Flags.FLAG_ENABLE_TASKBAR_NAVBAR_UNIFICATION, true); - enableTinyTaskbar = - properties.getBoolean(Flags.FLAG_ENABLE_TINY_TASKBAR, false); - onlyReuseBubbledTaskWhenLaunchedFromBubble = - properties.getBoolean(Flags.FLAG_ONLY_REUSE_BUBBLED_TASK_WHEN_LAUNCHED_FROM_BUBBLE, false); - } catch (NullPointerException e) { - throw new RuntimeException( - "Cannot read value from namespace multitasking " - + "from DeviceConfig. It could be that the code using flag " - + "executed before SettingsProvider initialization. Please use " - + "fixed read-only flag by adding is_fixed_read_only: true in " - + "flag declaration.", - e - ); - } - multitasking_is_cached = true; + public boolean bubbleViewInfoExecutors() { + return true; } @Override - - - public boolean animateBubbleSizeChange() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return animateBubbleSizeChange; - } - @Override - - - public boolean enableAppPairs() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableAppPairs; - - } - - @Override - - - public boolean enableBubbleAnything() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableBubbleAnything; - - } - - @Override - - - public boolean enableBubbleBar() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableBubbleBar; - - } - - @Override - - - public boolean enableBubbleStashing() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableBubbleStashing; - - } - - @Override - - - public boolean enableBubblesLongPressNavHandle() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableBubblesLongPressNavHandle; - - } - - @Override - - - public boolean enableLeftRightSplitInPortrait() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableLeftRightSplitInPortrait; - - } - - @Override - - - public boolean enableNewBubbleAnimations() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableNewBubbleAnimations; - - } - - @Override - - - public boolean enableOptionalBubbleOverflow() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableOptionalBubbleOverflow; - - } - - @Override - - - public boolean enablePip2Implementation() { + public boolean enableAutoTaskStackController() { return false; - } @Override - - + + + public boolean enableBubbleAnything() { + return false; + } + + @Override + + + public boolean enableBubbleBar() { + return false; + } + + @Override + + + public boolean enableBubbleBarOnPhones() { + return false; + } + + @Override + + + public boolean enableBubbleStashing() { + return false; + } + + @Override + + + public boolean enableBubbleTaskViewListener() { + return false; + } + + @Override + + + public boolean enableBubbleToFullscreen() { + return false; + } + + @Override + + + public boolean enableBubblesLongPressNavHandle() { + return false; + } + + @Override + + + public boolean enableCreateAnyBubble() { + return false; + } + + @Override + + + public boolean enableDynamicInsetsForAppLaunch() { + return false; + } + + @Override + + + public boolean enableFlexibleSplit() { + return false; + } + + @Override + + + public boolean enableFlexibleTwoAppSplit() { + return false; + } + + @Override + + + public boolean enableGsf() { + return true; + } + + @Override + + + public boolean enableMagneticSplitDivider() { + return false; + } + + @Override + + + public boolean enableNewBubbleAnimations() { + return false; + } + + @Override + + + public boolean enableOptionalBubbleOverflow() { + return false; + } + + @Override + + + public boolean enablePip2() { + return false; + } + + @Override + + public boolean enablePipUmoExperience() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enablePipUmoExperience; - + return false; } @Override - - + + + public boolean enableRecentsBookendTransition() { + return false; + } + + @Override + + public boolean enableRetrievableBubbles() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableRetrievableBubbles; - + return false; } @Override - - - public boolean enableSplitContextual() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableSplitContextual; + + public boolean enableShellTopTaskTracking() { + return false; } @Override - - + + + public boolean enableTaskViewControllerCleanup() { + return true; + } + + @Override + + public boolean enableTaskbarNavbarUnification() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableTaskbarNavbarUnification; - + return true; } @Override - - + + + public boolean enableTaskbarOnPhones() { + return true; + } + + @Override + + public boolean enableTinyTaskbar() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return enableTinyTaskbar; - + return false; } @Override - - - public boolean onlyReuseBubbledTaskWhenLaunchedFromBubble() { - if (isReadFromNew) { - if (!isCached) { - init(); - } - } else { - if (!multitasking_is_cached) { - load_overrides_multitasking(); - } - } - return onlyReuseBubbledTaskWhenLaunchedFromBubble; + + public boolean fixMissingUserChangeCallbacks() { + return false; + } + + @Override + + + public boolean onlyReuseBubbledTaskWhenLaunchedFromBubble() { + return true; + } + + @Override + + + public boolean taskViewRepository() { + return false; } } - diff --git a/flags/src/com/android/wm/shell/Flags.java b/flags/src/com/android/wm/shell/Flags.java index 5dd4ab7470..d5dbf382c4 100644 --- a/flags/src/com/android/wm/shell/Flags.java +++ b/flags/src/com/android/wm/shell/Flags.java @@ -1,119 +1,271 @@ package com.android.wm.shell; // TODO(b/303773055): Remove the annotation after access issue is resolved. + + /** @hide */ public final class Flags { /** @hide */ - public static final String FLAG_ANIMATE_BUBBLE_SIZE_CHANGE = "com.android.wm.shell.animate_bubble_size_change"; + public static final String FLAG_BUBBLE_VIEW_INFO_EXECUTORS = "com.android.wm.shell.bubble_view_info_executors"; /** @hide */ - public static final String FLAG_ENABLE_APP_PAIRS = "com.android.wm.shell.enable_app_pairs"; + public static final String FLAG_ENABLE_AUTO_TASK_STACK_CONTROLLER = "com.android.wm.shell.enable_auto_task_stack_controller"; /** @hide */ public static final String FLAG_ENABLE_BUBBLE_ANYTHING = "com.android.wm.shell.enable_bubble_anything"; /** @hide */ public static final String FLAG_ENABLE_BUBBLE_BAR = "com.android.wm.shell.enable_bubble_bar"; /** @hide */ + public static final String FLAG_ENABLE_BUBBLE_BAR_ON_PHONES = "com.android.wm.shell.enable_bubble_bar_on_phones"; + /** @hide */ public static final String FLAG_ENABLE_BUBBLE_STASHING = "com.android.wm.shell.enable_bubble_stashing"; /** @hide */ + public static final String FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER = "com.android.wm.shell.enable_bubble_task_view_listener"; + /** @hide */ + public static final String FLAG_ENABLE_BUBBLE_TO_FULLSCREEN = "com.android.wm.shell.enable_bubble_to_fullscreen"; + /** @hide */ public static final String FLAG_ENABLE_BUBBLES_LONG_PRESS_NAV_HANDLE = "com.android.wm.shell.enable_bubbles_long_press_nav_handle"; /** @hide */ - public static final String FLAG_ENABLE_LEFT_RIGHT_SPLIT_IN_PORTRAIT = "com.android.wm.shell.enable_left_right_split_in_portrait"; + public static final String FLAG_ENABLE_CREATE_ANY_BUBBLE = "com.android.wm.shell.enable_create_any_bubble"; + /** @hide */ + public static final String FLAG_ENABLE_DYNAMIC_INSETS_FOR_APP_LAUNCH = "com.android.wm.shell.enable_dynamic_insets_for_app_launch"; + /** @hide */ + public static final String FLAG_ENABLE_FLEXIBLE_SPLIT = "com.android.wm.shell.enable_flexible_split"; + /** @hide */ + public static final String FLAG_ENABLE_FLEXIBLE_TWO_APP_SPLIT = "com.android.wm.shell.enable_flexible_two_app_split"; + /** @hide */ + public static final String FLAG_ENABLE_GSF = "com.android.wm.shell.enable_gsf"; + /** @hide */ + public static final String FLAG_ENABLE_MAGNETIC_SPLIT_DIVIDER = "com.android.wm.shell.enable_magnetic_split_divider"; /** @hide */ public static final String FLAG_ENABLE_NEW_BUBBLE_ANIMATIONS = "com.android.wm.shell.enable_new_bubble_animations"; /** @hide */ public static final String FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW = "com.android.wm.shell.enable_optional_bubble_overflow"; /** @hide */ - public static final String FLAG_ENABLE_PIP2_IMPLEMENTATION = "com.android.wm.shell.enable_pip2_implementation"; + public static final String FLAG_ENABLE_PIP2 = "com.android.wm.shell.enable_pip2"; /** @hide */ public static final String FLAG_ENABLE_PIP_UMO_EXPERIENCE = "com.android.wm.shell.enable_pip_umo_experience"; /** @hide */ + public static final String FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION = "com.android.wm.shell.enable_recents_bookend_transition"; + /** @hide */ public static final String FLAG_ENABLE_RETRIEVABLE_BUBBLES = "com.android.wm.shell.enable_retrievable_bubbles"; /** @hide */ - public static final String FLAG_ENABLE_SPLIT_CONTEXTUAL = "com.android.wm.shell.enable_split_contextual"; + public static final String FLAG_ENABLE_SHELL_TOP_TASK_TRACKING = "com.android.wm.shell.enable_shell_top_task_tracking"; + /** @hide */ + public static final String FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP = "com.android.wm.shell.enable_task_view_controller_cleanup"; /** @hide */ public static final String FLAG_ENABLE_TASKBAR_NAVBAR_UNIFICATION = "com.android.wm.shell.enable_taskbar_navbar_unification"; /** @hide */ + public static final String FLAG_ENABLE_TASKBAR_ON_PHONES = "com.android.wm.shell.enable_taskbar_on_phones"; + /** @hide */ public static final String FLAG_ENABLE_TINY_TASKBAR = "com.android.wm.shell.enable_tiny_taskbar"; /** @hide */ + public static final String FLAG_FIX_MISSING_USER_CHANGE_CALLBACKS = "com.android.wm.shell.fix_missing_user_change_callbacks"; + /** @hide */ public static final String FLAG_ONLY_REUSE_BUBBLED_TASK_WHEN_LAUNCHED_FROM_BUBBLE = "com.android.wm.shell.only_reuse_bubbled_task_when_launched_from_bubble"; - - - public static boolean animateBubbleSizeChange() { - return FEATURE_FLAGS.animateBubbleSizeChange(); + /** @hide */ + public static final String FLAG_TASK_VIEW_REPOSITORY = "com.android.wm.shell.task_view_repository"; + + + + public static boolean bubbleViewInfoExecutors() { + + return FEATURE_FLAGS.bubbleViewInfoExecutors(); } - - - public static boolean enableAppPairs() { - return FEATURE_FLAGS.enableAppPairs(); + + + + public static boolean enableAutoTaskStackController() { + + return FEATURE_FLAGS.enableAutoTaskStackController(); } - - + + + public static boolean enableBubbleAnything() { + return FEATURE_FLAGS.enableBubbleAnything(); } - - + + + public static boolean enableBubbleBar() { + return FEATURE_FLAGS.enableBubbleBar(); } - - + + + + public static boolean enableBubbleBarOnPhones() { + + return FEATURE_FLAGS.enableBubbleBarOnPhones(); + } + + + public static boolean enableBubbleStashing() { + return FEATURE_FLAGS.enableBubbleStashing(); } - - + + + + public static boolean enableBubbleTaskViewListener() { + + return FEATURE_FLAGS.enableBubbleTaskViewListener(); + } + + + + public static boolean enableBubbleToFullscreen() { + + return FEATURE_FLAGS.enableBubbleToFullscreen(); + } + + + public static boolean enableBubblesLongPressNavHandle() { + return FEATURE_FLAGS.enableBubblesLongPressNavHandle(); } - - - public static boolean enableLeftRightSplitInPortrait() { - return FEATURE_FLAGS.enableLeftRightSplitInPortrait(); + + + + public static boolean enableCreateAnyBubble() { + + return FEATURE_FLAGS.enableCreateAnyBubble(); } - - + + + + public static boolean enableDynamicInsetsForAppLaunch() { + + return FEATURE_FLAGS.enableDynamicInsetsForAppLaunch(); + } + + + + public static boolean enableFlexibleSplit() { + + return FEATURE_FLAGS.enableFlexibleSplit(); + } + + + + public static boolean enableFlexibleTwoAppSplit() { + + return FEATURE_FLAGS.enableFlexibleTwoAppSplit(); + } + + + + public static boolean enableGsf() { + + return FEATURE_FLAGS.enableGsf(); + } + + + + public static boolean enableMagneticSplitDivider() { + + return FEATURE_FLAGS.enableMagneticSplitDivider(); + } + + + public static boolean enableNewBubbleAnimations() { + return FEATURE_FLAGS.enableNewBubbleAnimations(); } - - + + + public static boolean enableOptionalBubbleOverflow() { + return FEATURE_FLAGS.enableOptionalBubbleOverflow(); } - - public static boolean enablePip2Implementation() { - return FEATURE_FLAGS.enablePip2Implementation(); + + + + public static boolean enablePip2() { + + return FEATURE_FLAGS.enablePip2(); } - - + + + public static boolean enablePipUmoExperience() { + return FEATURE_FLAGS.enablePipUmoExperience(); } - - + + + + public static boolean enableRecentsBookendTransition() { + + return FEATURE_FLAGS.enableRecentsBookendTransition(); + } + + + public static boolean enableRetrievableBubbles() { + return FEATURE_FLAGS.enableRetrievableBubbles(); } - - - public static boolean enableSplitContextual() { - return FEATURE_FLAGS.enableSplitContextual(); + + + + public static boolean enableShellTopTaskTracking() { + + return FEATURE_FLAGS.enableShellTopTaskTracking(); } - - + + + + public static boolean enableTaskViewControllerCleanup() { + + return FEATURE_FLAGS.enableTaskViewControllerCleanup(); + } + + + public static boolean enableTaskbarNavbarUnification() { + return FEATURE_FLAGS.enableTaskbarNavbarUnification(); } - - + + + + public static boolean enableTaskbarOnPhones() { + + return FEATURE_FLAGS.enableTaskbarOnPhones(); + } + + + public static boolean enableTinyTaskbar() { + return FEATURE_FLAGS.enableTinyTaskbar(); } - - + + + + public static boolean fixMissingUserChangeCallbacks() { + + return FEATURE_FLAGS.fixMissingUserChangeCallbacks(); + } + + + public static boolean onlyReuseBubbledTaskWhenLaunchedFromBubble() { + return FEATURE_FLAGS.onlyReuseBubbledTaskWhenLaunchedFromBubble(); } + + + public static boolean taskViewRepository() { + + return FEATURE_FLAGS.taskViewRepository(); + } + private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); } diff --git a/go/AndroidManifest-launcher.xml b/go/AndroidManifest-launcher.xml index 8e427d4c31..cc4072e262 100644 --- a/go/AndroidManifest-launcher.xml +++ b/go/AndroidManifest-launcher.xml @@ -56,6 +56,7 @@ android:enabled="true"> + diff --git a/go/quickstep/res/values-af/strings.xml b/go/quickstep/res/values-af/strings.xml index 501d297a8d..f4ce476582 100644 --- a/go/quickstep/res/values-af/strings.xml +++ b/go/quickstep/res/values-af/strings.xml @@ -1,7 +1,7 @@ - "Deel program" + "Deel app" "Luister" "Vertaal" "Lens" @@ -9,12 +9,12 @@ "KANSELLEER" "INSTELLINGS" "Vertaal of luister na teks op skerm" - "Inligting soos teks op jou skerm, webadresse en skermskote kan met Google gedeel word.\n\nGaan na ""Instellings > Programme > Verstekprogramme > Digitale Assistent-program"" om te verander watter inligting jy deel." + "Inligting soos teks op jou skerm, webadresse en skermskote kan met Google gedeel word.\n\nGaan na ""Instellings > Apps > Verstekapps > Digitale Assistent-app"" om te verander watter inligting jy deel." "Kies \'n assistent om hierdie kenmerk te gebruik" "Kies \'n digitalebystandprogram in Instellings om na teks op jou skerm te luister of dit te vertaal" "Verander jou assistent om hierdie kenmerk te gebruik" "Verander jou digitalebystandprogram in Instellings om na teks op jou skerm te luister of dit te vertaal" "Tik hier om na teks op hierdie skerm te luister" "Tik hier om teks op hierdie skerm te vertaal" - "Hierdie program kan nie gedeel word nie" + "Hierdie app kan nie gedeel word nie" diff --git a/go/quickstep/res/values-be/strings.xml b/go/quickstep/res/values-be/strings.xml index 83374bb397..de8766f708 100644 --- a/go/quickstep/res/values-be/strings.xml +++ b/go/quickstep/res/values-be/strings.xml @@ -4,7 +4,7 @@ "Абагуліць праграму" "Праслухаць" "Перакласці" - "Аб\'ектыў" + "Аб’ектыў" "ЗРАЗУМЕЛА" "СКАСАВАЦЬ" "НАЛАДЫ" diff --git a/go/quickstep/res/values-fa/strings.xml b/go/quickstep/res/values-fa/strings.xml index 8453d4e3e2..f0e4a57712 100644 --- a/go/quickstep/res/values-fa/strings.xml +++ b/go/quickstep/res/values-fa/strings.xml @@ -5,7 +5,7 @@ "گوش دادن" "ترجمه" "لنز" - "متوجه‌ام" + "متوجهم" "لغو" "تنظیمات" "ترجمه نوشتار روی صفحه‌نمایش یا گوش دادن به آن" diff --git a/go/quickstep/res/values-iw/strings.xml b/go/quickstep/res/values-iw/strings.xml index ddb8ddd9a9..db661066e0 100644 --- a/go/quickstep/res/values-iw/strings.xml +++ b/go/quickstep/res/values-iw/strings.xml @@ -14,7 +14,7 @@ "כדי להאזין לטקסט שבמסך או לתרגם אותו, צריך לבחור אפליקציית עוזר דיגיטלי ב\'הגדרות\'" "צריך לשנות את העוזר הדיגיטלי כדי להשתמש בתכונה הזו" "כדי להאזין לטקסט שבמסך או לתרגם אותו, צריך לשנות את אפליקציית העוזר הדיגיטלי ב\'הגדרות\'" - "צריך להקיש כאן כדי להאזין לטקסט שבמסך הזה" - "צריך להקיש כאן כדי לתרגם את הטקסט שבמסך הזה" + "צריך ללחוץ כאן כדי להאזין לטקסט שבמסך הזה" + "צריך ללחוץ כאן כדי לתרגם את הטקסט שבמסך הזה" "אי אפשר לשתף את האפליקציה הזו" diff --git a/go/quickstep/res/values-ne/strings.xml b/go/quickstep/res/values-ne/strings.xml index e66f06373d..4f771c3ec4 100644 --- a/go/quickstep/res/values-ne/strings.xml +++ b/go/quickstep/res/values-ne/strings.xml @@ -9,11 +9,11 @@ "रद्द गर्नुहोस्" "सेटिङ" "स्क्रिनमा देखिने पाठ अनुवाद गर्नुहोस् वा पढेर सुनाउनुहोस्" - "तपाईंको स्क्रिनमा देखिने पाठ, वेब ठेगाना र स्क्रिनसटलगायतका जानकारी Google सँग सेयर गर्न सकिन्छ।\n\nकुन कुन जानकारी सेयर गर्न दिने भन्ने सेटिङ बदल्न ""सेटिङ > एप > डिफल्ट एप > डिजिटल सहायक एप"" मा जानुहोस्।" + "तपाईंको स्क्रिनमा देखिने पाठ, वेब ठेगाना र स्क्रिनसटलगायतका जानकारी Google सँग सेयर गर्न सकिन्छ।\n\nकुन कुन जानकारी सेयर गर्न दिने भन्ने सेटिङ बदल्न ""सेटिङ > एप > डिफल्ट एप > डिजिटल एसिस्टेन्ट एप"" मा जानुहोस्।" "तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने कुनै सहायक छनौट गर्नुहोस्" - "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप छनौट गर्नुहोस्" + "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल एसिस्टेन्ट एप छनौट गर्नुहोस्" "तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने आफ्नो सहायक परिवर्तन गर्नुहोस्" - "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप परिर्वर्तन गर्नुहोस्" + "तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल एसिस्टेन्ट एप परिर्वर्तन गर्नुहोस्" "तपाईं यो स्क्रिनमा देखिने पाठ सुन्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्" "तपाईं यो स्क्रिनमा देखिने पाठ अनुवाद गर्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्" "यो एप अरूलाई चलाउन दिन मिल्दैन" diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml index c659331bde..2524c760f4 100644 --- a/go/quickstep/res/values/styles.xml +++ b/go/quickstep/res/values/styles.xml @@ -16,7 +16,7 @@ --> - + \ No newline at end of file diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml index ca44a6914c..505bba9b43 100644 --- a/quickstep/res/values-nl/strings.xml +++ b/quickstep/res/values-nl/strings.xml @@ -22,11 +22,14 @@ "Vastzetten" "Vrije vorm" "Desktop" + "Verplaatsen naar extern scherm" + "Wissen" + "Desktop" "Geen recente items" "Instellingen voor app-gebruik" "Alles wissen" + "Nieuw bureau toevoegen" "Recente apps" - "Taak gesloten" "%1$s, %2$s" "< 1 minuut" "Nog %1$s vandaag" @@ -45,6 +48,7 @@ "App-suggesties staan aan" "App-suggesties staan uit" "Voorspelde app: %1$s" + "Tutorial voor navigatie met gebaren" "Het apparaat draaien" "Draai het apparaat om de tutorial voor navigatie met gebaren af te ronden" "Swipe vanaf de rechter- of linkerrand" @@ -56,17 +60,15 @@ "Open Instellingen om de gevoeligheid van Terug te wijzigen" "Swipe om terug te gaan" "Swipe vanaf de linker- of rechterrand naar het midden om terug te gaan naar het vorige scherm." - "Als je wilt teruggaan naar het laatste scherm, swipe je met 2 vingers vanaf de linker- of rechterrand naar het midden van het scherm." "Terug" "Swipe vanaf de linker- of rechterrand naar het midden van het scherm" "Swipe vanaf de onderrand van het scherm omhoog" "Pauzeer niet voordat je loslaat" "Swipe recht omhoog" "Je weet nu hoe je het gebaar Naar startscherm maakt. Ontdek als volgende hoe je kunt teruggaan." - "Je weet nu hoe je teruggaat naar het startscherm" + "Je weet nu hoe je het gebaar Naar het startscherm maakt" "Swipe om naar het startscherm te gaan" "Swipe omhoog vanaf de onderkant van het scherm. Met dit gebaar ga je altijd naar het startscherm." - "Swipe met 2 vingers omhoog vanaf de onderkant van het scherm. Met dit gebaar ga je altijd naar het startscherm." "Naar het startscherm" "Swipe omhoog vanaf de onderkant van het scherm" "Goed gedaan" @@ -77,8 +79,7 @@ "Je weet nu hoe je het gebaar Schakelen tussen apps maakt" "Swipe om tussen apps te schakelen" "Swipe omhoog vanaf de onderkant van het scherm, houd vast en laat los om tussen apps te schakelen." - "Swipe met 2 vingers omhoog vanaf de onderkant van het scherm, houd vast en laat los om tussen apps te schakelen." - "Schakelen tussen apps" + "Wisselen van app" "Swipe omhoog vanaf de onderkant van het scherm, houd je vinger op het scherm en laat dan los" "Goed bezig" "Klaar" @@ -91,24 +92,33 @@ "Swipe omhoog om naar het startscherm te gaan" "Tik op de startknop om naar je startscherm te gaan" "Je bent klaar om je %1$s te gebruiken" - "apparaat" + "Je bent klaar om je apparaat te gebruiken" "Navigatie-instellingen van systeem" + "Je %1$s is \ngereed." + "Het apparaat is \ngereed." + "Veel plezier met je nieuwe %1$s." + "Kiezen hoe je wilt navigeren" + "Omhoog swipen" + "Tik op de startknop" + "tablet" + "telefoon" "Delen" "Screenshot" "Splitsen" "App-paar opslaan" "Tik op nog een app om je scherm te splitsen" "Kies een andere app om gesplitst scherm te gebruiken" - "Annuleren" + "Annuleren" "Sluit de selectie voor gesplitst scherm" "Kies andere app om gesplitst scherm te gebruiken" "Deze actie wordt niet toegestaan door de app of je organisatie" "Widgets worden op dit moment niet ondersteund, selecteer een andere app" - "Navigatietutorial overslaan?" - "Je vindt dit later terug in de app %1$s" - "Annuleren" - "Overslaan" "Scherm draaien" + "Animatie die toont hoe de taakbalk onderaan het scherm verschijnt en automatisch wordt verborgen als deze niet wordt gebruikt" + "Animatie die toont hoe je de taakbalk vastzet met een schakelaar, zodat de taakbalk altijd zichtbaar blijft onderaan het scherm" + "Animatie die toont hoe je een gesplitst scherm maakt door een app van de taakbalk naar een geopende app te slepen" + "Animatie die toont hoe je toegang krijgt tot voorgestelde apps op je apparaat" + "Animatie die toont hoe je een item op het scherm kunt zoeken door de actietoets ingedrukt te houden en het gebied te selecteren waarin het item zich bevindt" "Taakbalk Onderwijs" "Sleep een app naar de zijkant om 2 apps tegelijk te gebruiken" "Swipe langzaam omhoog om de taakbalk te tonen" @@ -130,18 +140,40 @@ "Snelle instellingen" "Taakbalk" "Taakbalk wordt getoond" - "Taakbalk is verborgen" + "Taakbalk en bubbels links getoond" + "Taakbalk en bubbels rechts getoond" "Navigatiebalk" "Taakbalk altijd tonen" "Navigatiemodus wijzigen" "Scheiding voor Taakbalk" + "Andere recente apps" "Naar boven/links verplaatsen" "Naar beneden/rechts verplaatsen" - "{count,plural, =1{Nog # app tonen.}other{Nog # apps tonen.}}" - "{count,plural, =1{# desktop-app tonen.}other{# desktop-apps tonen.}}" + "App openen als ballon" + "Recente apps" + "Lijst met recente apps" + "{count,plural, =1{extra app}other{extra apps}}" + "Desktop" "%1$s en %2$s" + "%1$s, item %2$d van %3$d" + "Naar links scrollen" + "Naar rechts scrollen" "Bubbel" "Overloop" "%1$s van %2$s" "%1$s en nog %2$d" + "Naar links verplaatsen" + "Naar rechts verplaatsen" + "Alles sluiten" + "%1$s uitvouwen" + "%1$s samenvouwen" + "Circle to Search" + "Icoon van app" + "Titel van app" + "Knop Sluiten" + "Vastzetten op taakbalk" + "Losmaken van taakbalk" + "Verschuiven" + "Sluiten" + "Afbeelding verschuiven" diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml index bf0bdc8323..85a51ed369 100644 --- a/quickstep/res/values-or/strings.xml +++ b/quickstep/res/values-or/strings.xml @@ -22,11 +22,14 @@ "ପିନ୍‍" "ଫ୍ରିଫର୍ମ" "ଡେସ୍କଟପ" + "ଏକ୍ସଟର୍ନଲ ଡିସପ୍ଲେକୁ ମୁଭ କରନ୍ତୁ" + "ଖାଲି କରନ୍ତୁ" + "ଡେସ୍କଟପ" "ବର୍ତ୍ତମାନର କୌଣସି ଆଇଟମ ନାହିଁ" "ଆପ ବ୍ୟବହାର ସେଟିଂସ" "ସବୁ ଖାଲି କରନ୍ତୁ" + "ନୂଆ ଡେସ୍କ ଯୋଗ କରନ୍ତୁ" "ବର୍ତ୍ତମାନର ଆପ୍‌" - "ଟାସ୍କ ବନ୍ଦ ହୋଇଯାଇଛି" "%1$s %2$s" "< 1 ମିନିଟ୍" "ଆଜି %1$s ବାକି ଅଛି" @@ -45,18 +48,18 @@ "ଆପ୍ ପରାମର୍ଶଗୁଡ଼ିକୁ ସକ୍ଷମ କରାଯାଇଛି" "ଆପ୍ ପରାମର୍ଶଗୁଡ଼ିକୁ ଅକ୍ଷମ କରାଯାଇଛି" "ପୂର୍ବାନୁମାନ କରାଯାଇଥିବା ଆପ୍: %1$s" + "ଜେଶ୍ଚର ନାଭିଗେସନ ଟ୍ୟୁଟୋରିଆଲ" "ଆପଣଙ୍କ ଡିଭାଇସକୁ ରୋଟେଟ କରନ୍ତୁ" "ଜେଶ୍ଚର ନାଭିଗେସନ ଟ୍ୟୁଟୋରିଆଲ ସମ୍ପୂର୍ଣ୍ଣ କରିବାକୁ ଦୟାକରି ଆପଣଙ୍କ ଡିଭାଇସ ରୋଟେଟ କରନ୍ତୁ" - "ଆପଣ ସ୍କ୍ରିନର ଏକଦମ୍-ଡାହାଣ ବା ବାମ ଧାରରୁ ସ୍ୱାଇପ୍ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" + "ଆପଣ ସ୍କ୍ରିନର ଏକଦମ-ଡାହାଣ ବା ବାମ ଧାରରୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" "ଆପଣ ସ୍କ୍ରିନର ଡାହାଣ ବା ବାମ ଧାରରୁ ମଝିକୁ ସ୍ୱାଇପ କରି ଛାଡ଼ି ଦେଉଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ଆପଣ ଡାହାଣରୁ ସ୍ୱାଇପ୍ କରି ପଛକୁ କିପରି ଫେରିବେ ତାହା ଜାଣିଲେ। ତା\'ପରେ, ଆପକୁ କିପରି ସ୍ୱିଚ୍ କରିବେ ତାହା ଜାଣନ୍ତୁ।" "ଆପଣ \'ପଛକୁ ଫେରନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି। ତା\'ପରେ, ଆପଗୁଡ଼ିକୁ କିପରି ସ୍ୱିଚ୍ କରିବେ ତାହା ଜାଣନ୍ତୁ।" - "ଆପଣ \'ପଛକୁ ଫେରନ୍ତୁ\' ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି" + "ଆପଣ ପଛକୁ ଫେରନ୍ତୁ ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି" "ଆପଣ ସ୍କ୍ରିନର ତଳଭାଗର ଅତି ନିକଟରୁ ସ୍ୱାଇପ କରୁନଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ପଛକୁ ଫେରିବା ଜେଶ୍ଚରର ସମ୍ବେଦନଶୀଳତା ବଦଳାଇବାକୁ ସେଟିଂସକୁ ଯାଆନ୍ତୁ" "ପଛକୁ ଫେରିବା ପାଇଁ ସ୍ୱାଇପ୍ କରନ୍ତୁ" "ପୂର୍ବ ସ୍କ୍ରିନକୁ ଫେରିବା ପାଇଁ, ସ୍କ୍ରିନର ବାମ କିମ୍ବା ଡାହାଣ ଧାରରୁ ମଝିକୁ ସ୍ୱାଇପ କରନ୍ତୁ।" - "ପୂର୍ବ ସ୍କ୍ରିନକୁ ଫେରିବା ପାଇଁ, ସ୍କ୍ରିନର ବାମ କିମ୍ବା ଡାହାଣ ଧାରରୁ ମଝିକୁ 2ଟି ଆଙ୍ଗୁଠିରେ ସ୍ୱାଇପ କରନ୍ତୁ।" "ପଛକୁ ଫେରନ୍ତୁ" "ସ୍କ୍ରିନର ବାମ କିମ୍ବା ଡାହାଣ ଧାରରୁ ମଝିକୁ ସ୍ୱାଇପ କରନ୍ତୁ" "ଆପଣ ସ୍କ୍ରିନର ତଳ ଧାରରୁ ଉପରକୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" @@ -66,20 +69,18 @@ "ଆପଣ \'ମୂଳପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି।" "ହୋମକୁ ଯିବା ପାଇଁ ସ୍ୱାଇପ କରନ୍ତୁ" "ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ। ଏହି ଜେଶ୍ଚର ସର୍ବଦା ଆପଣଙ୍କୁ ହୋମ ସ୍କ୍ରିନକୁ ନେଇଥାଏ।" - "2ଟି ଆଙ୍ଗୁଠିରେ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ। ଏହି ଜେଶ୍ଚର ସର୍ବଦା ଆପଣଙ୍କୁ ହୋମ ସ୍କ୍ରିନକୁ ନେଇଥାଏ।" "ହୋମକୁ ଯାଆନ୍ତୁ" "ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ" "ବଢ଼ିଆ କାମ!" "ଆପଣ ସ୍କ୍ରିନର ତଳ ଧାରରୁ ଉପରକୁ ସ୍ୱାଇପ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ" "ୱିଣ୍ଡୋକୁ ରିଲିଜ୍ କରିବା ପୂର୍ବରୁ ଅଧିକ ସମୟ ଧରି ରଖିବାକୁ ଚେଷ୍ଟା କରନ୍ତୁ।" - "ଆପଣ ସିଧା ଉପରକୁ ସ୍ୱାଇପ୍ କରି ତା\'ପରେ ବିରତ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" + "ଆପଣ ସିଧା ଉପରକୁ ସ୍ୱାଇପ କରି ତା\'ପରେ ବିରତ କରୁଥିବା ସୁନିଶ୍ଚିତ କରନ୍ତୁ।" "ଜେଶ୍ଚରଗୁଡ଼ିକୁ କିପରି ବ୍ୟବହାର କରାଯିବ ଆପଣ ତାହା ଶିଖିଛନ୍ତି। ଜେଶ୍ଚରଗୁଡ଼ିକୁ ବନ୍ଦ କରିବାକୁ, ସେଟିଂସକୁ ଯାଆନ୍ତୁ।" - "ଆପଣ \'ଆପଗୁଡ଼ିକୁ ସ୍ୱିଚ୍ କରନ୍ତୁ\' ଜେଶ୍ଚର୍ ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି।" + "ଆପଣ ଆପ୍ସ ସୁଇଚ କରନ୍ତୁ ଜେଶ୍ଚର ସମ୍ପୂର୍ଣ୍ଣ କରିଛନ୍ତି।" "ଆପଗୁଡ଼ିକୁ ସ୍ୱିଚ୍ କରିବା ପାଇଁ ସ୍ୱାଇପ୍ କରନ୍ତୁ" "ଆପ୍ସ ମଧ୍ୟରେ ସୁଇଚ କରିବାକୁ, ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ, ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ କରନ୍ତୁ।" - "ଆପ୍ସ ମଧ୍ୟରେ ସ୍ୱିଚ କରିବାକୁ, 2ଟି ଆଙ୍ଗୁଠିରେ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ କର।" "ଆପ୍ସ ସୁଇଚ କରନ୍ତୁ" - "ଆପଣଙ୍କ ସ୍କ୍ରିନ୍‌ର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ୍ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ୍ କରନ୍ତୁ" + "ଆପଣଙ୍କ ସ୍କ୍ରିନର ତଳୁ ଉପରକୁ ସ୍ୱାଇପ କରି ଧରି ରଖନ୍ତୁ, ତା\'ପରେ ରିଲିଜ କରନ୍ତୁ" "ବହୁତ ବଢ଼ିଆ!" "ସବୁ ପ୍ରସ୍ତୁତ" "ହୋଇଗଲା" @@ -91,24 +92,33 @@ "ହୋମକୁ ଯିବା ପାଇଁ ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ" "ଆପଣଙ୍କ ହୋମ ସ୍କ୍ରିନକୁ ଯିବା ପାଇଁ ହୋମ ବଟନରେ ଟାପ କରନ୍ତୁ" "ଆପଣ ଆପଣଙ୍କ %1$s ବ୍ୟବହାର କରିବା ଆରମ୍ଭ କରିବାକୁ ପ୍ରସ୍ତୁତ ଅଛନ୍ତି" - "ଡିଭାଇସ" + "ଆପଣ ଆପଣଙ୍କ ଡିଭାଇସ ବ୍ୟବହାର କରିବା ଆରମ୍ଭ କରିବାକୁ ପ୍ରସ୍ତୁତ ଅଛନ୍ତି" "ସିଷ୍ଟମ ନାଭିଗେସନ ସେଟିଂସ" + "ଆପଣଙ୍କ %1$s \nପ୍ରସ୍ତୁତ ଅଛି!" + "ଆପଣଙ୍କ ଡିଭାଇସ \nପ୍ରସ୍ତୁତ ଅଛି!" + "ଆପଣଙ୍କ ନୂଆ %1$sର ମଜା ନିଅନ୍ତୁ!" + "କିପରି ନାଭିଗେଟ କରିବେ ତାହା ବାଛନ୍ତୁ" + "ଉପରକୁ ସ୍ୱାଇପ କରନ୍ତୁ" + "ହୋମ ବଟନରେ ଟାପ କରନ୍ତୁ" + "ଟାବଲେଟ" + "ଫୋନ" "ସେୟାର୍ କରନ୍ତୁ" "ସ୍କ୍ରିନସଟ୍" "ସ୍ପ୍ଲିଟ୍" "ଆପ ପେୟାର ସେଭ କରନ୍ତୁ" "ସ୍ପ୍ଲିଟସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପରେ ଟାପ କର" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" - "ବାତିଲ କରନ୍ତୁ" + "ବାତିଲ କରନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ଚୟନରୁ ବାହାରି ଯାଆନ୍ତୁ" "ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ବ୍ୟବହାର କରିବାକୁ ଅନ୍ୟ ଏକ ଆପ ବାଛନ୍ତୁ" "ଆପ୍ କିମ୍ବା ଆପଣଙ୍କ ସଂସ୍ଥା ଦ୍ୱାରା ଏହି କାର୍ଯ୍ୟକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ" "ୱିଜେଟଗୁଡ଼ିକ ବର୍ତ୍ତମାନ ସମର୍ଥିତ ନୁହେଁ, ଦୟାକରି ଅନ୍ୟ ଏକ ଆପ ଚୟନ କରନ୍ତୁ" - "ନାଭିଗେସନ୍ ଟ୍ୟୁଟୋରିଆଲକୁ ବାଦ୍ ଦେବେ?" - "ଆପଣ ପରେ ଏହାକୁ %1$s ଆପରେ ପାଇପାରିବେ" - "ବାତିଲ କରନ୍ତୁ" - "ବାଦ୍ ଦିଅନ୍ତୁ" "ସ୍କ୍ରିନ ଘୂରାନ୍ତୁ" + "ସ୍କ୍ରିନର ନିମ୍ନରୁ ଟାସ୍କବାର କିପରି ଦେଖାଯାଏ ଏବଂ ବ୍ୟବହାରରେ ନଥିବା ସମୟରେ ସ୍ୱତଃ ଲୁଚିଯାଏ ତାହା ଦେଖାଉଥିବା ଆନିମେସନ" + "ଏକ ଟୋଗଲ ବ୍ୟବହାର କରି ଆପଣଙ୍କର ଟାସ୍କବାରକୁ କିପରି ପିନ କରିବେ ତାହା ଦେଖାଉଥିବା ଆନିମେସନ, ଫଳରେ ଟାସ୍କବାର ସ୍କ୍ରିନର ନିମ୍ନରେ ସ୍ଥାୟୀ ଭାବରେ ଦେଖାଯିବ" + "ଏକ ଖୋଲା ଥିବା ଆପ ଉପରେ ଟାସ୍କବାରରୁ ଏକ ଆପ ଡ୍ରାଗ ଏବଂ ଡ୍ରପ କରି ଏକ ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ କିପରି ତିଆରି କରିବେ ତାହା ଦେଖାଉଥିବା ଆନିମେସନ" + "ଆପଣଙ୍କ ଡିଭାଇସରେ ପ୍ରସ୍ତାବିତ ଆପ୍ସକୁ କିପରି ଆକ୍ସେସ କରିବେ ତାହା ଦେଖାଉଥିବା ଆନିମେସନ" + "ଆକ୍ସନ କୀ\'କୁ ସ୍ପର୍ଶ କରି ଧରି ରଖି ଏବଂ ଆଇଟମ କେଉଁ ଏରିଆରେ ଅଛି ତାହା ଚୟନ କରି ସ୍କ୍ରିନରେ ଏକ ଆଇଟମ କିପରି ସର୍ଚ୍ଚ କରିବେ ତାହା ଦେଖାଉଥିବା ଆନିମେସନ" "ଟାସ୍କବାର ଶିକ୍ଷା" "ଥରକେ 2ଟି ଆପ୍ସ ବ୍ୟବହାର କରିବା ପାଇଁ ଏକ ଆପକୁ ପାର୍ଶ୍ୱକୁ ଡ୍ରାଗ କର" "ଟାସ୍କବାର ଦେଖାଇବା ପାଇଁ ଉପରକୁ ଧୀରେ-ସ୍ୱାଇପ କରନ୍ତୁ" @@ -130,18 +140,40 @@ "କୁଇକ ସେଟିଂସ" "ଟାସ୍କବାର" "ଟାସ୍କବାର ଦେଖାଯାଇଛି" - "ଟାସ୍କବାର ଲୁଚାଯାଇଛି" + "ଟାସ୍କବାର ଓ ବବଲ ବାମରେ ଦେଖାଯାଇଛି" + "ଟାସ୍କବାର ଓ ବବଲ ଡାହାଣରେ ଦେଖାଯାଇଛି" "ନାଭିଗେସନ ବାର" "ସର୍ବଦା ଟାସ୍କବାର ଦେଖାନ୍ତୁ" "ନାଭିଗେସନ ମୋଡ ପରିବର୍ତ୍ତନ କରନ୍ତୁ" "ଟାସ୍କବାର ଡିଭାଇଡର" + "ଅନ୍ୟ ବର୍ତ୍ତମାନର ଆପ୍ସ" "ଶୀର୍ଷ/ବାମକୁ ମୁଭ କରନ୍ତୁ" "ନିମ୍ନ/ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ" - "{count,plural, =1{ଅଧିକ #ଟି ଆପ ଦେଖାନ୍ତୁ।}other{ଅଧିକ #ଟି ଆପ୍ସ ଦେଖାନ୍ତୁ।}}" - "{count,plural, =1{# ଡେସ୍କଟପ ଆପ ଦେଖାନ୍ତୁ।}other{# ଡେସ୍କଟପ ଆପ୍ସ ଦେଖାନ୍ତୁ।}}" + "ଏକ ବବଲ ଭାବେ ଆପ ଖୋଲନ୍ତୁ" + "ବର୍ତ୍ତମାନର ଆପ" + "ବର୍ତ୍ତମାନର ଆପ ତାଲିକା" + "{count,plural, =1{ଅଧିକ ଆପ}other{ଅଧିକ ଆପ୍ସ}}" + "ଡେସ୍କଟପ" "%1$s ଏବଂ %2$s" + "%1$s, %3$d%2$d ଆଇଟମ" + "ବାମକୁ ସ୍କ୍ରୋଲ କରନ୍ତୁ" + "ଡାହାଣକୁ ସ୍କ୍ରୋଲ କରନ୍ତୁ" "ବବଲ" "ଓଭରଫ୍ଲୋ" "%2$sରୁ %1$s" "%1$s ଏବଂ ଅଧିକ %2$d" + "ବାମକୁ ମୁଭ କରନ୍ତୁ" + "ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ" + "ସବୁ ଖାରଜ କରନ୍ତୁ" + "%1$s ବିସ୍ତାର କରନ୍ତୁ" + "%1$s ସଙ୍କୁଚିତ କରନ୍ତୁ" + "ସର୍ଚ୍ଚ କରିବାକୁ ସର୍କଲ କରନ୍ତୁ" + "ଆପ ଆଇକନ" + "ଆପ ଟାଇଟେଲ" + "\"ବନ୍ଦ କରନ୍ତୁ\" ବଟନ" + "ଟାସ୍କବାରରେ ପିନ କର" + "ଟାସ୍କବାରରୁ ଅନପିନ କର" + "ନଜ" + "ବନ୍ଦ କରନ୍ତୁ" + "ନଜ ଇମେଜ" diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml index fc603969a7..56abc74b0d 100644 --- a/quickstep/res/values-pa/strings.xml +++ b/quickstep/res/values-pa/strings.xml @@ -22,11 +22,14 @@ "ਪਿੰਨ ਕਰੋ" "ਫ੍ਰੀਫਾਰਮ" "ਡੈਸਕਟਾਪ" + "ਬਾਹਰੀ ਡਿਸਪਲੇ \'ਤੇ ਜਾਓ" + "ਕਲੀਅਰ ਕਰੋ" + "ਡੈਸਕਟਾਪ" "ਕੋਈ ਹਾਲੀਆ ਆਈਟਮ ਨਹੀਂ" "ਐਪ ਵਰਤੋਂ ਦੀਆਂ ਸੈਟਿੰਗਾਂ" "ਸਭ ਕਲੀਅਰ ਕਰੋ" + "ਨਵਾਂ ਡੈਸਕ ਸ਼ਾਮਲ ਕਰੋ" "ਹਾਲੀਆ ਐਪਾਂ" - "ਕਾਰਜ ਬੰਦ" "%1$s, %2$s" "< 1 ਮਿੰਟ" "ਅੱਜ %1$s ਬਾਕੀ" @@ -45,6 +48,7 @@ "ਐਪ ਸੁਝਾਵਾਂ ਨੂੰ ਚਾਲੂ ਕੀਤਾ ਗਿਆ" "ਐਪ ਸੁਝਾਵਾਂ ਨੂੰ ਬੰਦ ਕੀਤਾ ਗਿਆ" "ਪੂਰਵ ਅਨੁਮਾਨਿਤ ਐਪ: %1$s" + "ਇਸ਼ਾਰਾ ਨੈਵੀਗੇਸ਼ਨ ਸੰਬੰਧੀ ਟਿਊਟੋਰੀਅਲ" "ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਘੁੰਮਾਓ" "ਕਿਰਪਾ ਕਰਕੇ ਇਸ਼ਾਰਾ ਨੈਵੀਗੇਸ਼ਨ ਟਿਊਟੋਰੀਅਲ ਨੂੰ ਪੂਰਾ ਕਰਨ ਲਈ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਘੁੰਮਾਓ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸੱਜੇ ਜਾਂ ਖੱਬੇ ਪਾਸੇ ਦੇ ਬਿਲਕੁਲ ਕਿਨਾਰੇ ਤੋਂ ਸਵਾਈਪ ਕਰਦੇ ਹੋ" @@ -56,7 +60,6 @@ "ਪਿੱਛੇ ਜਾਣ ਦੇ ਸੰਕੇਤ ਦੀ ਸੰਵੇਦਨਸ਼ੀਲਤਾ ਬਦਲਣ ਲਈ, ਸੈਟਿੰਗਾਂ \'ਤੇ ਜਾਓ" "ਪਿੱਛੇ ਜਾਣ ਲਈ ਸਵਾਈਪ ਕਰੋ" "ਪਿਛਲੀ ਸਕ੍ਰੀਨ \'ਤੇ ਵਾਪਸ ਜਾਣ ਲਈ, ਖੱਬੇ ਜਾਂ ਸੱਜੇ ਕਿਨਾਰੇ ਤੋਂ ਸਕ੍ਰੀਨ ਦੇ ਵਿਚਕਾਰ ਤੱਕ ਸਵਾਈਪ ਕਰੋ।" - "ਪਿਛਲੀ ਸਕ੍ਰੀਨ \'ਤੇ ਵਾਪਸ ਜਾਣ ਲਈ, ਖੱਬੇ ਜਾਂ ਸੱਜੇ ਕਿਨਾਰੇ ਤੋਂ ਸਕ੍ਰੀਨ ਦੇ ਵਿਚਕਾਰ ਤੱਕ 2 ਉਂਗਲਾਂ ਨਾਲ ਸਵਾਈਪ ਕਰੋ।" "ਵਾਪਸ ਜਾਓ" "ਖੱਬੇ ਜਾਂ ਸੱਜੇ ਕਿਨਾਰੇ ਤੋਂ ਸਕ੍ਰੀਨ ਦੇ ਵਿਚਕਾਰ ਤੱਕ ਸਵਾਈਪ ਕਰੋ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਲੇ ਕਿਨਾਰੇ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" @@ -66,7 +69,6 @@ "ਤੁਸੀਂ \'ਹੋਮ \'ਤੇ ਜਾਓ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ" "ਹੋਮ \'ਤੇ ਜਾਣ ਲਈ ਸਵਾਈਪ ਕਰੋ" "ਆਪਣੀ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ। ਇਹ ਇਸ਼ਾਰਾ ਹਮੇਸ਼ਾਂ ਤੁਹਾਨੂੰ ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਲੈ ਜਾਂਦਾ ਹੈ।" - "ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ 2 ਉਂਗਲਾਂ ਨਾਲ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ। ਇਹ ਇਸ਼ਾਰਾ ਹਮੇਸ਼ਾਂ ਤੁਹਾਨੂੰ ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਲੈ ਜਾਂਦਾ ਹੈ।" "ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਜਾਓ" "ਆਪਣੀ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" "ਬਹੁਤ ਵਧੀਆ!" @@ -74,10 +76,9 @@ "ਛੱਡਣ ਤੋਂ ਪਹਿਲਾਂ ਵਿੰਡੋ ਨੂੰ ਕੁਝ ਸਮੇਂ ਲਈ ਦਬਾ ਕੇ ਰੱਖੋ" "ਇਹ ਪੱਕਾ ਕਰੋ ਕਿ ਤੁਸੀਂ ਸਿੱਧਾ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ, ਫਿਰ ਰੋਕੋ" "ਤੁਸੀਂ ਇਸ਼ਾਰੇ ਵਰਤਣ ਬਾਰੇ ਜਾਣਿਆ। ਇਸ਼ਾਰੇ ਬੰਦ ਕਰਨ ਲਈ, ਸੈਟਿੰਗਾਂ \'ਤੇ ਜਾਓ।" - "ਤੁਸੀਂ \'ਐਪਾਂ ਵਿਚਾਲੇ ਅਦਲਾ-ਬਦਲੀ ਕਰੋ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ" + "ਤੁਸੀਂ \'ਐਪਾਂ ਵਿਚਾਲੇ ਸਵਿੱਚ ਕਰੋ\' ਦਾ ਇਸ਼ਾਰਾ ਪੂਰਾ ਕੀਤਾ" "ਐਪਾਂ ਵਿਚਾਲੇ ਅਦਲਾ-ਬਦਲੀ ਕਰਨ ਲਈ ਸਵਾਈਪ ਕਰੋ" "ਐਪਾਂ ਵਿਚਾਲੇ ਸਵਿੱਚ ਕਰਨ ਲਈ, ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਤੇ ਸਵਾਈਪ ਕਰ ਕੇ ਦਬਾਈ ਰੱਖੋ ਅਤੇ ਫਿਰ ਛੱਡੋ।" - "ਐਪਾਂ ਵਿਚਾਲੇ ਸਵਿੱਚ ਕਰਨ ਲਈ, ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ 2 ਉਂਗਲਾਂ ਨਾਲ ਉੱਤੇ ਸਵਾਈਪ ਕਰ ਕੇ ਦਬਾਈ ਰੱਖੋ ਅਤੇ ਫਿਰ ਛੱਡੋ।" "ਐਪਾਂ ਸਵਿੱਚ ਕਰੋ" "ਆਪਣੀ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰ ਕੇ ਦਬਾਈ ਰੱਖੋ, ਅਤੇ ਫਿਰ ਛੱਡੋ" "ਬਹੁਤ ਵਧੀਆ!" @@ -90,25 +91,34 @@ "ਪੂਰੀ ਤਰ੍ਹਾਂ ਤਿਆਰ!" "ਹੋਮ \'ਤੇ ਜਾਣ ਲਈ ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" "ਆਪਣੀ ਹੋਮ ਸਕ੍ਰੀਨ \'ਤੇ ਜਾਣ ਲਈ ਹੋਮ ਬਟਨ \'ਤੇ ਟੈਪ ਕਰੋ" - "ਤੁਸੀਂ ਆਪਣਾ %1$s ਵਰਤਣ ਲਈ ਤਿਆਰ ਹੋ" - "ਡੀਵਾਈਸ" + "ਹੁਣ ਤੁਹਾਡਾ %1$s ਵਰਤੋਂ ਲਈ ਤਿਆਰ ਹੈ" + "ਤੁਸੀਂ ਆਪਣਾ ਡੀਵਾਈਸ ਵਰਤਣ ਲਈ ਤਿਆਰ ਹੋ" "ਸਿਸਟਮ ਨੈਵੀਗੇਸ਼ਨ ਸੈਟਿੰਗਾਂ" + "ਤੁਹਾਡਾ %1$s \nਤਿਆਰ ਹੈ!" + "ਤੁਹਾਡਾ ਡੀਵਾਈਸ \nਤਿਆਰ ਹੈ!" + "ਆਪਣੇ ਨਵੇਂ %1$s ਦਾ ਅਨੰਦ ਮਾਣੋ!" + "ਨੈਵੀਗੇਟ ਕਰਨ ਦਾ ਤਰੀਕਾ ਚੁਣੋ" + "ਉੱਪਰ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" + "ਹੋਮ ਬਟਨ \'ਤੇ ਟੈਪ ਕਰੋ" + "ਟੈਬਲੈੱਟ" + "ਫ਼ੋਨ" "ਸਾਂਝਾ ਕਰੋ" "ਸਕ੍ਰੀਨਸ਼ਾਟ" "ਸਪਲਿਟ" "ਐਪ ਜੋੜਾਬੱਧ ਰੱਖਿਅਤ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਨੂੰ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ \'ਤੇ ਟੈਪ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" - "ਰੱਦ ਕਰੋ" + "ਰੱਦ ਕਰੋ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਦੀ ਚੋਣ ਤੋਂ ਬਾਹਰ ਜਾਓ" "ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਰਤਣ ਲਈ ਕਿਸੇ ਹੋਰ ਐਪ ਨੂੰ ਚੁਣੋ" "ਐਪ ਜਾਂ ਤੁਹਾਡੀ ਸੰਸਥਾ ਵੱਲੋਂ ਇਸ ਕਾਰਵਾਈ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ" "ਫ਼ਿਲਹਾਲ ਵਿਜੇਟ ਸਮਰਥਿਤ ਨਹੀਂ ਹਨ, ਕਿਰਪਾ ਕਰਕੇ ਕੋਈ ਹੋਰ ਐਪ ਚੁਣੋ" - "ਕੀ ਨੈਵੀਗੇਸ਼ਨ ਟਿਊਟੋਰੀਅਲ ਨੂੰ ਛੱਡਣਾ ਹੈ?" - "ਤੁਸੀਂ ਇਸਨੂੰ ਬਾਅਦ ਵਿੱਚ %1$s ਐਪ ਵਿੱਚ ਲੱਭ ਸਕਦੇ ਹੋ" - "ਰੱਦ ਕਰੋ" - "ਛੱਡੋ" "ਸਕ੍ਰੀਨ ਘੁਮਾਓ" + "ਟਾਸਕਬਾਰ ਦੇ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਤੋਂ ਦ੍ਰਿਸ਼ ਵਿੱਚ ਆਉਣ ਅਤੇ ਵਰਤੋਂ ਵਿੱਚ ਨਾ ਹੋਣ \'ਤੇ ਆਪਣੇ ਆਪ ਲੁਕ ਜਾਣ ਦਾ ਤਰੀਕਾ ਦਿਖਾਉਣ ਵਾਲਾ ਐਨੀਮੇਸ਼ਨ" + "ਟੌਗਲ ਦੀ ਵਰਤੋਂ ਕਰ ਕੇ ਤੁਹਾਡੇ ਟਾਸਕਬਾਰ ਨੂੰ ਪਿੰਨ ਕਰਨ ਦਾ ਤਰੀਕਾ ਦਿਖਾਉਣ ਵਾਲਾ ਐਨੀਮੇਸ਼ਨ, ਤਾਂ ਜੋ ਟਾਸਕਬਾਰ ਪੱਕੇ ਤੌਰ \'ਤੇ ਸਕ੍ਰੀਨ ਦੇ ਹੇਠਾਂ ਦਿਖਣਯੋਗ ਰਹੇ" + "ਕਿਸੇ ਖੁੱਲ੍ਹੀ ਐਪ ਉੱਪਰ ਟਾਸਕਬਾਰ ਤੋਂ ਘਸੀਟ ਅਤੇ ਛੱਡ ਕੇ, ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਬਣਾਉਣ ਦਾ ਤਰੀਕਾ ਦਿਖਾਉਣ ਵਾਲਾ ਐਨੀਮੇਸ਼ਨ" + "ਤੁਹਾਡੇ ਡੀਵਾਈਸ \'ਤੇ ਸੁਝਾਈਆਂ ਗਈਆਂ ਐਪਾਂ ਤੱਕ ਪਹੁੰਚ ਕਰਨ ਦਾ ਤਰੀਕਾ ਦਿਖਾਉਣ ਵਾਲਾ ਐਨੀਮੇਸ਼ਨ" + "ਕਾਰਵਾਈ ਕੁੰਜੀ ਨੂੰ ਸਪਰਸ਼ ਕਰ ਕੇ ਰੱਖਦੇ ਹੋਏ ਅਤੇ ਆਈਟਮ ਜਿਸ ਖੇਤਰ ਵਿੱਚ ਹੈ ਉਸਨੂੰ ਚੁਣਦੇ ਹੋਏ, ਸਕ੍ਰੀਨ \'ਤੇ ਦਿੱਤੀ ਆਈਟਮ ਨੂੰ ਖੋਜਣ ਦਾ ਤਰੀਕਾ ਦਿਖਾਉਣ ਵਾਲਾ ਐਨੀਮੇਸ਼ਨ" "ਟਾਸਕਬਾਰ ਸਿੱਖਿਆ" "ਇੱਕੋ ਵੇਲੇ 2 ਐਪਾਂ ਵਰਤਣ ਲਈ, ਕਿਸੇ ਐਪ ਨੂੰ ਸਾਈਡ \'ਤੇ ਘਸੀਟੋ" "ਟਾਸਕਬਾਰ ਦਿਖਾਉਣ ਲਈ ਹੌਲੀ ਜਿਹੀ ਉੱਤੇ ਵੱਲ ਸਵਾਈਪ ਕਰੋ" @@ -130,18 +140,40 @@ "ਤਤਕਾਲ ਸੈਟਿੰਗਾਂ" "ਟਾਸਕਬਾਰ" "ਟਾਸਕਬਾਰ ਨੂੰ ਦਿਖਾਇਆ ਗਿਆ" - "ਟਾਸਕਬਾਰ ਨੂੰ ਲੁਕਾਇਆ ਗਿਆ" + "ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਖੱਬੇ ਦਿਖਾਇਆ" + "ਟਾਸਕਬਾਰ ਤੇ ਬਬਲ ਨੂੰ ਸੱਜੇ ਦਿਖਾਇਆ" "ਨੈਵੀਗੇਸ਼ਨ ਵਾਲੀ ਪੱਟੀ" "ਹਮੇਸ਼ਾਂ ਟਾਸਕਬਾਰ ਦਿਖਾਓ" "ਨੈਵੀਗੇਸ਼ਨ ਮੋਡ ਬਦਲੋ" "ਟਾਸਕਬਾਰ ਵਿਭਾਜਕ" + "ਹੋਰ ਹਾਲੀਆ ਐਪਾਂ" "ਸਿਖਰਲੇ/ਖੱਬੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ" "ਹੇਠਾਂ/ਸੱਜੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ" - "{count,plural, =1{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}one{# ਹੋਰ ਐਪ ਦਿਖਾਓ।}other{# ਹੋਰ ਐਪਾਂ ਦਿਖਾਓ।}}" - "{count,plural, =1{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}one{# ਡੈਸਕਟਾਪ ਐਪ ਦਿਖਾਓ।}other{# ਡੈਸਕਟਾਪ ਐਪਾਂ ਦਿਖਾਓ।}}" + "ਐਪ ਨੂੰ ਬਬਲ ਵਜੋਂ ਖੋਲ੍ਹੋ" + "ਹਾਲੀਆ ਐਪਾਂ" + "ਹਾਲੀਆ ਐਪ ਸੂਚੀ" + "{count,plural, =1{ਹੋਰ ਐਪ}one{ਹੋਰ ਐਪ}other{ਹੋਰ ਐਪਾਂ}}" + "ਡੈਸਕਟਾਪ" "%1$s ਅਤੇ %2$s" + "%1$s, %3$d ਵਿੱਚੋਂ %2$d ਆਈਟਮ" + "ਖੱਬੇ ਪਾਸੇ ਵੱਲ ਸਕ੍ਰੋਲ ਕਰੋ" + "ਸੱਜੇ ਪਾਸੇ ਵੱਲ ਸਕ੍ਰੋਲ ਕਰੋ" "ਬਬਲ" "ਓਵਰਫ਼ਲੋ" "%2$s ਤੋਂ %1$s" "%1$s ਅਤੇ %2$d ਹੋਰ" + "ਖੱਬੇ ਲਿਜਾਓ" + "ਸੱਜੇ ਲਿਜਾਓ" + "ਸਭ ਖਾਰਜ ਕਰੋ" + "%1$s ਦਾ ਵਿਸਤਾਰ ਕਰੋ" + "%1$s ਨੂੰ ਸਮੇਟੋ" + "ਖੋਜਣ ਲਈ ਚੱਕਰ ਬਣਾਓ" + "ਐਪ ਪ੍ਰਤੀਕ" + "ਐਪ ਸਿਰਲੇਖ" + "\'ਬੰਦ ਕਰੋ\' ਬਟਨ" + "ਟਾਸਕਬਾਰ \'ਤੇ ਪਿੰਨ ਕਰੋ" + "ਟਾਸਕਬਾਰ ਤੋਂ ਅਣਪਿੰਨ ਕਰੋ" + "ਰਿਮਾਈਂਡਰ" + "ਬੰਦ ਕਰੋ" + "ਰਿਮਾਈਂਡਰ ਦਾ ਚਿੱਤਰ" diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml index 13c8c1c48b..6d80a3feca 100644 --- a/quickstep/res/values-pl/strings.xml +++ b/quickstep/res/values-pl/strings.xml @@ -22,11 +22,14 @@ "Przypnij" "Tryb dowolny" "Pulpit" + "Przenieś na wyświetlacz zewnętrzny" + "Wyczyść" + "Pulpit" "Brak ostatnich elementów" "Ustawienia użycia aplikacji" "Wyczyść wszystko" + "Dodaj nowe biurko" "Ostatnie aplikacje" - "Zadanie zamknięte" "%1$s, %2$s" "> 1 min" "Na dziś zostało %1$s" @@ -45,6 +48,7 @@ "Włączono sugestie aplikacji" "Sugestie aplikacji są wyłączone" "Przewidywana aplikacja: %1$s" + "Samouczek dotyczący nawigacji przy użyciu gestów" "Obróć urządzenie" "Obróć urządzenie, aby ukończyć samouczek nawigacji przy użyciu gestów" "Pamiętaj, aby przesuwać palcem od samej krawędzi (prawej lub lewej)" @@ -56,7 +60,6 @@ "Czułość gestu cofania możesz zmienić w Ustawieniach" "Przesuń palcem, aby przejść wstecz" "Aby wrócić do poprzedniego ekranu, przesuń palcem od lewej lub prawej krawędzi do środka ekranu." - "Aby wrócić do poprzedniego ekranu, przesuń 2 palcami od lewej lub prawej krawędzi do środka ekranu." "Przejście wstecz" "Przesuń palcem od lewej lub prawej krawędzi do środka ekranu" "Pamiętaj, aby przesuwać palcem od dolnej krawędzi ekranu" @@ -66,7 +69,6 @@ "Gest przechodzenia na ekran główny został opanowany" "Przesuń palcem, aby przejść na ekran główny" "Przesuń palcem od dołu ekranu. Ten gest zawsze powoduje przejście na ekran główny." - "Przesuń 2 palcami od dołu ekranu. Ten gest zawsze powoduje przejście do ekranu głównego." "Przejście na ekran główny" "Przesuń palcem z dołu ekranu w górę" "Brawo!" @@ -77,8 +79,7 @@ "Gest przełączania aplikacji został opanowany" "Przesuń palcem, aby przełączać aplikacje" "Aby przełączać się między aplikacjami, przesuń palcem od dołu do góry ekranu, przytrzymaj i puść." - "Aby przełączać się między aplikacjami, przesuń 2 palcami od dołu ekranu, przytrzymaj i puść." - "Przełącz aplikację" + "Przełączanie aplikacji" "Przesuń palcem z dołu ekranu w górę, przytrzymaj i puść" "Brawo!" "Wszystko gotowe" @@ -88,30 +89,39 @@ "Super!" "Samouczek %1$d/%2$d" "Wszystko gotowe" - "Aby przejść na stronę główną, przesuń w górę" + "Aby przejść na ekran główny, przesuń w górę" "Kliknij przycisk ekranu głównego, aby otworzyć ekran główny" "%1$s jest gotowe – możesz zacząć z niego korzystać" - "Urządzenie" + "Wszystko gotowe – możesz zacząć korzystać z urządzenia" "Ustawienia nawigacji w systemie" + "Twój %1$s jest \ngotowy." + "Urządzenie jest \ngotowe." + "Nowy %1$s jest już gotowy do użycia." + "Wybierz sposób poruszania się po urządzeniu" + "Przesuń palcem w górę" + "Kliknij przycisk ekranu głównego" + "tablet" + "telefon" "Udostępnij" "Zrzut ekranu" "Podziel" "Zapisz parę" "Aby podzielić ekran, kliknij drugą aplikację" "Aby podzielić ekran, wybierz drugą aplikację" - "Anuluj" + "Anuluj" "Wyjdź z wyboru podzielonego ekranu" "Wybierz drugą aplikację, aby podzielić ekran" "Nie możesz wykonać tego działania, bo nie zezwala na to aplikacja lub Twoja organizacja" "Widżety nie są obecnie obsługiwane, wybierz inną aplikację" - "Pominąć samouczek nawigacji?" - "Znajdziesz to później w aplikacji %1$s" - "Anuluj" - "Pomiń" "Obróć ekran" + "Animacja pokazująca, jak pasek aplikacji pojawia się u dołu ekranu i automatycznie znika, gdy nie jest używany" + "Animacja pokazująca, jak za pomocą przełącznika przypiąć pasek aplikacji, aby był on stale widoczny u dołu ekranu" + "Animacja pokazująca, jak utworzyć podzielony ekran, przeciągając i upuszczając aplikację z paska aplikacji nad aktualnie otwartą" + "Animacja pokazująca, jak uzyskać dostęp do sugerowanych aplikacji na urządzeniu" + "Animacja pokazująca, jak wyszukać element na ekranie, naciskając i przytrzymując klawisz działania oraz wybierając obszar, w którym jest dany element" "Informacje o pasku aplikacji" "Przeciągnij aplikację na bok, aby używać 2 aplikacji naraz" - "Aby wyświetlić pasek aplikacji, powoli przesuń palcem w górę" + "Aby wyświetlić pasek aplikacji, wolno przesuń palcem w górę" "Otrzymuj sugestie aplikacji na podstawie typowych działań" "Przytrzymaj separator, aby przypiąć pasek aplikacji" "Wykorzystaj potencjał paska aplikacji" @@ -130,18 +140,40 @@ "Szybkie ustawienia" "Pasek aplikacji" "Pasek aplikacji widoczny" - "Pasek aplikacji ukryty" + "Pasek i dymki po lewej" + "Pasek i dymki po prawej" "Pasek nawigacyjny" "Zawsze pokazuj pasek aplikacji" "Zmień tryb nawigacji" "Linia dzielenia paska aplikacji" + "Inne ostatnio używane aplikacje" "Przesuń w górny lewy róg" "Przesuń w dolny prawy róg" - "{count,plural, =1{Pokaż jeszcze # aplikację.}few{Pokaż jeszcze # aplikacje.}many{Pokaż jeszcze # aplikacji.}other{Pokaż jeszcze # aplikacji.}}" - "{count,plural, =1{Pokaż # aplikację komputerową.}few{Pokaż # aplikacje komputerowe.}many{Pokaż # aplikacji komputerowych.}other{Pokaż # aplikacji komputerowej.}}" + "Otwórz aplikację jako dymek" + "Ostatnie aplikacje" + "Lista ostatnich aplikacji" + "{count,plural, =1{inna aplikacja}few{inne aplikacje}many{innych aplikacji}other{innej aplikacji}}" + "Pulpit" "%1$s%2$s" + "%1$s, element %2$d%3$d" + "Przewiń w lewo" + "Przewiń w prawo" "Dymek" "Rozwijany" "%1$s z aplikacji %2$s" "%1$s i jeszcze %2$d" + "Przenieś w lewo" + "Przenieś w prawo" + "Zamknij wszystkie" + "rozwiń dymek: %1$s" + "zwiń dymek: %1$s" + "Zaznacz, aby wyszukać" + "Ikona aplikacji" + "Tytuł aplikacji" + "Przycisk Zamknij" + "Przypnij do paska" + "Odepnij od paska" + "Ponaglenie" + "Zamknij" + "Obraz ponaglenia" diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml index e4d07bd97a..00213b04d9 100644 --- a/quickstep/res/values-pt-rPT/strings.xml +++ b/quickstep/res/values-pt-rPT/strings.xml @@ -22,11 +22,14 @@ "Fixar" "Forma livre" "Computador" + "Mover para o ecrã externo" + "Limpar" + "Computador" "Nenhum item recente" "Definições de utilização de aplicações" "Limpar tudo" + "Adicionar novo espaço de trabalho" "Apps recentes" - "Tarefa fechada" "%1$s, %2$s" "< 1 minuto" "Resta(m) %1$s hoje." @@ -45,9 +48,10 @@ "Sugestões de apps ativadas" "As sugestões de apps estão desativadas" "App prevista: %1$s" + "Tutorial da navegação por gestos" "Rode o dispositivo" "Rode o seu dispositivo para concluir o tutorial de navegação por gestos" - "Deslize rapidamente a partir da extremidade mais à direita ou mais à esquerda" + "Deslize a partir da extremidade mais à direita ou mais à esquerda" "Deslize rapidamente a partir da extremidade esquerda ou direita até ao centro do ecrã e solte" "Aprendeu a deslizar a partir da direita para retroceder. A seguir, saiba como alternar entre apps." "Concluiu o gesto para retroceder. A seguir, saiba como alternar entre apps." @@ -56,30 +60,27 @@ "Altere a sensibilidade do gesto para voltar nas Definições." "Deslize rapidamente com o dedo para retroceder" "Para voltar ao último ecrã, deslize rapidamente do limite esquerdo ou direito até ao centro do ecrã." - "Para voltar ao último ecrã, deslize rapidamente com 2 dedos a partir da extremidade esquerda ou direita até ao centro do ecrã." "Retroceder" - "Deslize rapidamente a partir da extremidade esquerda ou direita para o meio do ecrã" - "Deslize rapidamente com o dedo a partir do limite inferior do ecrã" + "Deslize a partir da extremidade esquerda ou direita até ao centro do ecrã" + "Deslize a partir do limite inferior do ecrã" "Não faça uma pausa antes de soltar" "Deslize rapidamente com o dedo para cima" "Concluiu o gesto para aceder ao ecrã principal. A seguir, saiba como retroceder." "Concluiu o gesto para aceder ao ecrã principal" "Deslize rapidamente com o dedo para aceder ao ecrã principal" "Deslize rapidamente para cima a partir da parte inferior. Este gesto abre sempre o ecrã principal." - "Deslize rapidamente para cima com 2 dedos no fundo do ecrã. Este gesto abre sempre o ecrã principal." "Aceda ao ecrã principal" - "Deslize rapidamente para cima a partir da parte inferior do ecrã" + "Deslize para cima a partir da parte inferior do ecrã" "Muito bem!" - "Deslize rapidamente com o dedo a partir do limite inferior do ecrã" + "Deslize a partir do limite inferior do ecrã" "Experimente premir a janela durante mais tempo antes de soltar" - "Garanta que desliza rapidamente com o dedo para cima e, em seguida, faz uma pausa" + "Deslize para cima e pause" "Aprendeu a utilizar gestos. Para desativar os gestos, aceda às Definições." "Concluiu o gesto para alternar entre apps" "Deslize rapidamente com o dedo para alternar entre apps" "Para alternar entre apps, deslize para cima sem soltar a partir da parte inferior do ecrã e solte." - "Para mudar de app, deslize rapidamente para cima com 2 dedos sem soltar no fundo do ecrã e solte." "Mude de app" - "Deslize rapidamente para cima a partir da parte inferior do ecrã sem soltar e, em seguida, solte" + "Deslize para cima a partir da parte inferior do ecrã, detenha o gesto e solte" "Muito bem!" "Está tudo pronto" "Concluído" @@ -91,24 +92,33 @@ "Deslize rapidamente para cima para aceder ao ecrã principal" "Toque no botão do ecrã principal para aceder ao ecrã principal" "Já pode começar a usar o seu %1$s" - "dispositivo" + "Já pode começar a usar o seu dispositivo" "Definições de navegação do sistema" + "O seu %1$s está \npronto!" + "O seu dispositivo está \npronto!" + "Desfrute do seu novo %1$s!" + "Escolha como navegar" + "Deslize rapidamente para cima" + "Toque no botão do ecrã principal" + "tablet" + "telemóvel" "Partilhar" - "Fazer captura de ecrã" + "Captura de ecrã" "Dividir" "Guardar par de apps" "Toque noutra app para usar o ecrã dividido" "Escolha outra app para usar o ecrã dividido" - "Cancelar" + "Cancelar" "Saia da seleção de ecrã dividido" "Escolher outra app para usar o ecrã dividido" "Esta ação não é permitida pela app ou a sua entidade." "Os widgets não são atualmente suportados. Selecione outra app" - "Ignorar o tutorial de navegação?" - "Pode encontrar isto mais tarde na app %1$s" - "Cancelar" - "Ignorar" "Rodar ecrã" + "Animação que mostra como a Barra de tarefas aparece na parte inferior do ecrã e é ocultada automaticamente quando não está em utilização" + "Animação que mostra como afixar a Barra de tarefas através de um botão, para que a Barra de tarefas fique permanentemente visível na parte inferior do ecrã" + "Animação que mostra como criar um ecrã dividido arrastando e largando uma app da Barra de tarefas sobre uma app aberta" + "Animação que mostra como aceder às apps sugeridas no seu dispositivo" + "Animação que mostra como pesquisar um item no ecrã tocando sem soltar na tecla de ação e selecionando a área onde o item se encontra" "Educação da Barra de tarefas" "Arraste uma app para o lado para usar 2 apps em simultâneo" "Deslize lentamente para cima para ver a Barra de tarefas" @@ -124,24 +134,46 @@ "Início" "Acessibilidade" "Voltar" - "Comutador IME" + "Switcher IME" "Recentes" "Notificações" "Definiç. rápidas" "Barra de tarefas" "Barra de tarefas apresentada" - "Barra de tarefas ocultada" + "Barra de tarefas/balões à esq." + "Barra de tarefas/balões à dir." "Barra de navegação" "Ver sempre Barra de tarefas" "Alterar modo de navegação" "Divisor da Barra de tarefas" + "Outras apps recentes" "Mover para a parte superior esquerda" "Mover para a part superior direita" - "{count,plural, =1{Mostrar mais # app.}other{Mostrar mais # apps.}}" - "{count,plural, =1{Mostrar # app para computador.}other{Mostrar # apps para computador.}}" + "Abrir app como um balão" + "Apps recentes" + "Lista de apps recentes" + "{count,plural, =1{outra app}other{outras apps}}" + "Computador" "%1$s e %2$s" + "%1$s, item %2$d de %3$d" + "Deslocar para a esquerda" + "Deslocar para a direita" "Balão" "Menu adicional" "%1$s da app %2$s" "%1$s e mais %2$d pessoas" + "Mover para a esquerda" + "Mover para a direita" + "Ignorar tudo" + "expandir %1$s" + "reduzir %1$s" + "Circundar para Pesquisar" + "Ícone da app" + "Título da app" + "Botão Fechar" + "Afixar na barra tar." + "Desaf. da barra tar." + "Chamada de atenção" + "Fechar" + "Imagem de chamada de atenção" diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml index 4fec4f8511..90b6785293 100644 --- a/quickstep/res/values-pt/strings.xml +++ b/quickstep/res/values-pt/strings.xml @@ -21,12 +21,15 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Fixar" "Forma livre" - "Computador" + "Modo área de trabalho" + "Mover para a tela externa" + "Limpar" + "Computador" "Nenhum item recente" "Configurações de uso do app" "Remover tudo" + "Adicionar novo espaço de trabalho" "Apps recentes" - "Tarefa encerrada" "%1$s, %2$s" "< 1 min" "%1$s restante(s) hoje" @@ -45,6 +48,7 @@ "O recurso \"sugestões de apps\" está ativado" "O recurso \"sugestões de apps\" está desativado" "App previsto: %1$s" + "Tutorial da navegação por gestos" "Gire o dispositivo" "Gire o dispositivo para concluir o tutorial da navegação por gestos" "Deslize da borda direita ou esquerda" @@ -56,7 +60,6 @@ "Mude a sensibilidade do gesto de voltar nas configurações" "Deslize para voltar" "Para voltar à tela anterior, deslize da borda esquerda ou direita até o meio da tela." - "Para voltar à tela anterior, deslize da borda esquerda ou direita até o meio da tela com dois dedos." "Volte" "Deslize da borda esquerda ou direita até o meio da tela" "Deslize da borda inferior da tela para cima" @@ -66,18 +69,16 @@ "Você concluiu o gesto para acessar a tela inicial" "Deslizar para voltar à tela inicial" "Deslize de baixo para cima na tela. Esse gesto sempre leva você para a tela inicial." - "Deslize de baixo para cima na tela com dois dedos. Esse gesto sempre leva você para a tela inicial." "Vá para a página inicial" "Deslize de baixo para cima na tela" "Muito bem!" "Deslize da borda inferior da tela para cima" "Mantenha a janela pressionada por mais tempo antes de soltar" - "Deslize para cima e pare" + "Deslize para cima em linha reta e pare" "Você aprendeu. Para desativar os gestos, acesse as Configurações." "Você concluiu o gesto para mudar de app" "Deslizar para trocar de app" "Para mudar de app, deslize de baixo para cima, mantenha a tela pressionada por um tempo e solte." - "Para mudar de app, deslize de baixo para cima na tela com dois dedos, segure por um tempo e solte." "Mude de app" "Deslize de baixo para cima na tela, segure e depois solte" "Muito bem!" @@ -89,26 +90,35 @@ "Tutorial %1$d/%2$d" "Tudo pronto!" "Deslize para cima para acessar a tela inicial" - "Toque no botão home para ir para a tela inicial" - "Você já pode começar a usar seu %1$s" - "dispositivo" + "Toque no botão home para acessar a tela inicial" + "O %1$s já pode ser usado" + "Você já pode começar a usar o dispositivo" "Configurações de navegação do sistema" + "O %1$s está \npronto." + "O dispositivo está \npronto." + "Aproveite seu novo dispositivo %1$s." + "Escolha como navegar" + "Deslize para cima" + "Toque no botão home" + "tablet" + "smartphone" "Compartilhar" "Capturar tela" "Dividir" "Salvar par de apps" "Toque em outro app para usar a tela dividida" "Escolha outro app para usar na tela dividida" - "Cancelar" + "Cancelar" "Sair da seleção de tela dividida" "Escolha outro app para usar na tela dividida" "Essa ação não é permitida pelo app ou pela organização" "Atualmente, não há suporte para widgets. Selecione outro app" - "Pular o tutorial de navegação?" - "Veja o tutorial mais tarde no app %1$s" - "Cancelar" - "Pular" "Girar a tela" + "Animação mostrando como a barra de tarefas aparece na parte de baixo da tela e é ocultada automaticamente quando não está em uso" + "Animação mostrando como fixar a barra de tarefas usando um botão de ativar/desativar para que ela fique sempre visível na parte de baixo da tela" + "Animação mostrando como criar uma tela dividida arrastando e soltando um app da barra de tarefas sobre outro aberto" + "Animação mostrando como acessar os apps sugeridos no dispositivo" + "Animação mostrando como pesquisar um item na tela tocando na tecla de ação, pressionando e selecionando a área em que o item está" "Informações sobre a barra de tarefas" "Arraste um app para o lado e use dois apps ao mesmo tempo" "Deslize para cima devagar para mostrar a Barra de tarefas" @@ -118,7 +128,7 @@ "Sempre mostrar a Barra de tarefas" "Toque e pressione o divisor para sempre mostrar a Barra de tarefas na parte de baixo da tela" "Toque na tecla de ação e pressione para pesquisar o que está na tela" - "O produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <a href="%1$s">Política de Privacidade</a> e aos <a href="%2$s">Termos de Serviço</a> do Google." + "Este produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <a href="%1$s">Política de Privacidade</a> e aos <a href="%2$s">Termos de Serviço</a> do Google." "Fechar" "Concluído" "Início" @@ -130,18 +140,40 @@ "Config. rápidas" "Barra de tarefas" "Barra de tarefas visível" - "Barra de tarefas oculta" + "Barra de tar. e balões à esq." + "Barra de tar. e balões à dir." "Barra de navegação" "Sempre mostrar a Barra de tarefas" "Mudar o modo de navegação" "Separador da Barra de tarefas" + "Outros apps recentes" "Mover para cima/para a esquerda" "Mover para baixo/para a direita" - "{count,plural, =1{Mostrar mais # app.}one{Mostrar mais # app.}other{Mostrar mais # apps.}}" - "{count,plural, =1{Mostrar # app para computador.}one{Mostrar # app para computador.}other{Mostrar # apps para computador.}}" + "Abrir o app como um balão" + "Apps recentes" + "Lista de apps recentes" + "{count,plural, =1{outro app}one{outro app}other{outros apps}}" + "Computador" "%1$s e %2$s" + "%1$s, item %2$d de %3$d" + "Rolar para a esquerda" + "Rolar para a direita" "Balão" "Balão flutuante" "%1$s do app %2$s" "%1$s e mais %2$d" + "Mover para esquerda" + "Mover para direita" + "Dispensar todos" + "abrir %1$s" + "fechar %1$s" + "Circule para Pesquisar" + "Ícone do app" + "Título do app" + "Botão \"Fechar\"" + "Fixar na barra de tarefas" + "Liberar da barra de tarefas" + "Alerta" + "Fechar" + "Imagem de alerta" diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml index c839602e89..a8ab473352 100644 --- a/quickstep/res/values-ro/strings.xml +++ b/quickstep/res/values-ro/strings.xml @@ -21,12 +21,15 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Fixează" "Formă liberă" - "Computer" + "Desktop" + "Mută pe ecranul extern" + "Șterge" + "Computer" "Niciun element recent" "Setări de utilizare a aplicației" "Șterge tot" + "Adaugă un nou desktop" "Aplicații recente" - "Activitatea s-a încheiat" "%1$s, %2$s" "< 1 minut" "Au mai rămas %1$s astăzi" @@ -45,6 +48,7 @@ "Sugestiile de aplicații au fost activate" "Sugestiile de aplicații au fost dezactivate" "Aplicația estimată: %1$s" + "Tutorial de navigare prin gesturi" "Rotește dispozitivul" "Rotește dispozitivul pentru a încheia tutorialul de navigare prin gesturi" "Glisează dinspre marginea dreaptă îndepărtată sau dinspre marginea stângă îndepărtată" @@ -56,7 +60,6 @@ "Schimbă sensibilitatea gestului „Înapoi” accesând Setările" "Glisează pentru a reveni" "Pentru a reveni la ultimul ecran, glisează de la marginea stângă sau dreaptă spre mijlocul ecranului." - "Pentru a reveni la ultimul ecran, glisează cu două degete dinspre marginea stângă sau dreaptă spre mijlocul ecranului." "Înapoi" "Glisează dinspre marginea stângă sau dreaptă până la jumătatea ecranului" "Glisează în sus dinspre marginea de jos a ecranului" @@ -66,7 +69,6 @@ "Ai finalizat gestul „accesează ecranul de pornire”" "Glisează pentru a accesa ecranul de pornire" "Glisează în sus din partea de jos a ecranului. Cu acest gest accesezi mereu ecranul de pornire." - "Glisează în sus cu două degete din partea de jos. Cu acest gest accesezi mereu ecranul de pornire." "Înapoi la ecranul de pornire" "Glisează în sus din partea de jos a ecranului" "Excelent!" @@ -77,7 +79,6 @@ "Ai finalizat gestul „comută între aplicații”" "Glisează pentru a comuta între aplicații" "Ca să comuți între aplicații, glisează de jos în sus, ține degetul pe ecran, apoi ridică-l." - "Ca să comuți între aplicații, glisează cu 2 degete de jos în sus, ține-le pe ecran, apoi ridică-le." "Comută între aplicații" "Glisează în sus din partea de jos a ecranului, ține apăsat, apoi eliberează" "Felicitări!" @@ -91,24 +92,33 @@ "Glisează în sus pentru a accesa ecranul principal" "Atinge butonul ecran de pornire ca să accesezi ecranul de pornire" "Ești gata să folosești %1$s" - "dispozitivul" + "Ești gata să folosești dispozitivul" "Setările de navigare ale sistemului" + "%1$s este \ngata!" + "Dispozitivul este \ngata!" + "Bucură-te de noul %1$s!" + "Alege cum să navighezi" + "Glisează în sus" + "Atinge butonul ecran de pornire" + "tabletă" + "telefon" "Distribuie" "Captură de ecran" "Împărțit" "Salvează perechea de aplicații" "Atinge altă aplicație pentru ecranul împărțit" "Alege altă aplicație pentru a folosi ecranul împărțit" - "Anulează" + "Anulează" "Ieși din selecția cu ecran împărțit" "Alege altă aplicație pentru ecranul împărțit" "Această acțiune nu este permisă de aplicație sau de organizația ta" "Nu se acceptă widgeturile. Selectează altă aplicație." - "Omiți tutorialul de navigare?" - "Îl poți găsi mai târziu în aplicația %1$s" - "Anulează" - "Omite" "Rotește ecranul" + "Animație care arată cum bara de activități apare din partea de jos a ecranului și se ascunde automat când nu este folosită" + "Animație care arată cum să fixezi bara de activități folosind un comutator, astfel încât bara de activități să rămână permanent vizibilă în partea de jos a ecranului" + "Animație care arată cum să creezi un ecran împărțit, trăgând și plasând o aplicație din bara de activități deasupra unei aplicații deschise" + "Animație care arată cum să accesezi aplicațiile sugerate pe dispozitiv" + "Animație care arată cum să cauți un element pe ecran, atingând lung tasta de acțiuni și selectând zona în care se află elementul" "Informații despre bara de activități" "Trage în lateral o aplicație ca să folosești 2 aplicații deodată" "Glisează lent în sus pentru a afișa bara de activități" @@ -130,18 +140,40 @@ "Setări rapide" "Bară de activități" "Bara de activități este afișată" - "Bara de activități este ascunsă" + "Bară și baloane stânga afișate" + "Bară & baloane dreapta afișate" "Bară de navigare" "Afișează mereu bara" "Schimbă modul de navigare" "Separator pentru bara de activități" + "Alte aplicații recente" "Mută în stânga sus" "Mută în dreapta jos" - "{count,plural, =1{Afișează încă # aplicație}few{Afișează încă # aplicații}other{Afișează încă # de aplicații}}" - "{count,plural, =1{Afișează # aplicație pentru computer.}few{Afișează # aplicații pentru computer.}other{Afișează # de aplicații pentru computer.}}" + "Deschide aplicația ca balon" + "Aplicații recente" + "Lista de aplicații recente" + "{count,plural, =1{aplicație suplimentară}few{mai multe aplicații}other{mai multe aplicații}}" + "Computer" "%1$s și %2$s" + "%1$s, articolul %2$d din %3$d" + "Derulează la stânga" + "Derulează la dreapta" "Balon" "Suplimentar" "%1$s de la %2$s" "%1$s și încă %2$d" + "Deplasează spre stânga" + "Deplasează spre dreapta" + "Închide-le pe toate" + "extinde %1$s" + "restrânge %1$s" + "Încercuiește și caută" + "Pictograma aplicației" + "Titlul aplicației" + "Buton de închidere" + "Fixează pe bara de activități" + "Anulează fixarea din bara de activități" + "Reafișează" + "Închide" + "Reafișează imaginea" diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml index da49ad32b8..09104f1499 100644 --- a/quickstep/res/values-ru/strings.xml +++ b/quickstep/res/values-ru/strings.xml @@ -21,12 +21,15 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Закрепить" "Произвольная форма" - "Включить режим для ПК" + "Мультиоконный режим" + "Перенести на внешний дисплей" + "Очистить" + "Мультиоконный режим" "Здесь пока ничего нет." "Настройки использования приложения" "Очистить все" + "Добавить рабочий стол" "Недавние приложения" - "Задача закрыта" "%1$s: %2$s" "< 1 мин." "Осталось сегодня: %1$s" @@ -45,6 +48,7 @@ "Функция \"Рекомендуемые приложения\" включена." "Функция \"Рекомендуемые приложения\" отключена." "Рекомендуемое приложение: %1$s" + "Руководство: навигация с помощью жестов" "Поверните устройство" "Чтобы перейти к руководству по жестам, нужно повернуть устройство." "Проведите справа налево или слева направо от самого края экрана." @@ -56,7 +60,6 @@ "Уровень чувствительности можно изменить в настройках." "Возврат к предыдущему экрану" "Чтобы вернуться к предыдущему экрану, проведите от левого или правого края дисплея к центру." - "Чтобы вернуться на предыдущий экран, проведите двумя пальцами от левого или правого края экрана к центру." "Как вернуться к предыдущему экрану" "Проведите от левого или правого края экрана к центру." "Проведите снизу вверх от самого края экрана." @@ -66,18 +69,16 @@ "Вы выполнили жест для перехода на главный экран." "Переход на главный экран" "Проведите вверх от нижнего края дисплея. Этот жест открывает главный экран." - "Проведите двумя пальцами вверх от нижнего края экрана. Этот жест открывает главный экран." "Как перейти на главный экран" "Проведите вверх от нижнего края экрана." "У вас получилось!" "Проведите снизу вверх от самого края экрана." "Прежде чем отпустить палец, задержите его на экране немного дольше." - "Проведите по экрану ровно вверх и задержите палец в конце." + "Проведите по экрану вверх и задержите палец." "Теперь вы знаете, как использовать жесты. Чтобы отключить их, перейдите в настройки." "Вы выполнили жест для переключения между приложениями." "Переключение между приложениями" "Чтобы переключиться между приложениями‚ проведите по экрану снизу вверх, задержите палец, а затем отпустите." - "Чтобы сменить приложение, проведите двумя пальцами снизу вверх, задержите пальцы, а затем отпустите." "Как переключаться между приложениями" "Проведите вверх от нижнего края экрана, задержите палец в одной точке и отпустите." "Отлично!" @@ -91,24 +92,33 @@ "Чтобы перейти на главный экран, проведите вверх." "Нажмите кнопку главного экрана, чтобы открыть его." "Теперь вы можете использовать %1$s." - "устройство" - "Системные настройки навигации" + "Теперь устройство можно использовать." + "Настройки навигации в системе" + "Устройство (%1$s)\nготово!" + "Устройство\nготово!" + "Начните использовать %1$s." + "Выбрать способ навигации" + "Проведите по экрану вверх" + "Нажмите кнопку главного экрана" + "планшет" + "телефон" "Поделиться" "Скриншот" "Разделить" "Сохранить приложения" "Для разделения экрана выберите другое приложение." "Чтобы использовать разделенный экран, выберите другое приложение." - "Отмена" + "Отмена" "Выйдите из режима разделения экрана." "Выберите другое приложение для разделения экрана." "Это действие заблокировано приложением или организацией." "Виджеты не поддерживаются. Выберите другое приложение." - "Пропустить руководство по жестам?" - "Его можно найти в приложении \"%1$s\"." - "Отмена" - "Пропустить" "Повернуть экран" + "Анимация: панель задач появляется в нижней части экрана и автоматически скрывается, если не используется" + "Анимация: как с помощью переключателя закрепить панель задач в нижней части экрана" + "Анимация: как разделить экран, перетащив приложение с панели задач на открытое приложение" + "Анимация: как перейти к рекомендуемым приложениям на устройстве" + "Анимация: как найти информацию об объекте на экране. Удерживая клавишу действия, нужно выбрать область, в которой находится объект." "Обучение по работе с панелью задач" "Используйте два приложения сразу, перетащив одно в сторону." "Чтобы открыть панель задач, медленно проведите снизу вверх." @@ -130,18 +140,40 @@ "Быстрые настройки" "Панель задач" "Панель задач показана" - "Панель задач скрыта" + "Слева панель задач, подсказки" + "Справа панель задач, подсказки" "Панель навигации" "Всегда показывать панель задач" "Изменить режим навигации" "Разделитель панели задач" + "Другие недавние приложения" "Переместить вверх или влево" "Переместить вниз или вправо" - "{count,plural, =1{Показать ещё # приложение}one{Показать ещё # приложение}few{Показать ещё # приложения}many{Показать ещё # приложений}other{Показать ещё # приложения}}" - "{count,plural, =1{Показать # компьютерное приложение.}one{Показать # компьютерное приложение.}few{Показать # компьютерных приложения.}many{Показать # компьютерных приложений.}other{Показать # компьютерного приложения.}}" + "Открыть приложение во всплывающем окне" + "Недавние приложения" + "Список недавних приложений" + "{count,plural, =1{дополнительное приложение}one{дополнительное приложение}few{дополнительных приложения}many{дополнительных приложений}other{дополнительного приложения}}" + "Режим компьютера" "%1$s и %2$s" + "%1$s, элемент %2$d из %3$d" + "Прокрутить влево" + "Прокрутить вправо" "Всплывающая подсказка" "Дополнительное меню" "\"%1$s\" из приложения \"%2$s\"" "%1$s и ещё %2$d" + "Переместить влево" + "Переместить вправо" + "Закрыть все" + "Развернуто: %1$s" + "Свернуто: %1$s" + "Обвести и найти" + "Значок приложения" + "Название приложения" + "Кнопка \"Закрыть\"" + "Закрепить на панели" + "Открепить от панели" + "Напоминание" + "Закрыть" + "Изображение напоминания" diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml index 9cbe837f94..b9bc3d381c 100644 --- a/quickstep/res/values-si/strings.xml +++ b/quickstep/res/values-si/strings.xml @@ -22,11 +22,14 @@ "අමුණන්න" "Freeform" "ඩෙස්ක්ටොපය" + "බාහිර සංදර්ශකය වෙත ගෙන යන්න" + "පැහැදිලි" + "ඩෙස්ක්ටොපය" "මෑත අයිතම නැත" "යෙදුම් භාවිත සැකසීම්" "සියල්ල හිස් කරන්න" + "නව මේසයක් එක් කරන්න" "මෑත යෙදුම්" - "කාර්යය අවසන් කරන ලදි" "%1$s, %2$s" "< 1 විනාඩියක්" "අද %1$sක් ඉතුරුයි" @@ -45,6 +48,7 @@ "යෙදුම් යෝජනා සබලිතයි" "යෙදුම් යෝජනා අබල කර ඇත" "පුරෝකථනය කළ යෙදුම: %1$s" + "අභින සංචාලන නිබන්ධනය" "ඔබේ උපාංගය කරකවන්න" "අභින සංචාලන නිබන්ධනය සම්පූර්ණ කිරීම සඳහා ඔබේ උපාංගය කරකවන්න" "ඔබ ඈත දකුණු හෝ ඈත වම් දාරයේ සිට ස්වයිප් කරන බව සහතික කර ගන්න" @@ -56,7 +60,6 @@ "ආපසු ඉංගිතයෙහි සංවේදීතාව වෙනස් කිරීමට, සැකසීම් වෙත යන්න" "ආපසු යාමට ස්වයිප් කරන්න" "අවසාන තිරයට ආපසු යාමට, වම් හෝ දකුණු දාරයෙන් තිරයේ මැදට ස්වයිප් කරන්න." - "අවසාන තිරයට ආපසු යාමට, වම් හෝ දකුණු දාරයෙන් තිරයේ මැදට ඇඟිලි 2කින් ස්වයිප් කරන්න." "ආපසු යන්න" "වම් හෝ දකුණු කෙළවරේ සිට තිරයේ මැදට ස්වයිප් කරන්න" "ඔබ තිරයේ පහළ දාරයේ සිට ඉහළට ස්වයිප් කරන බව සහතික කර ගන්න" @@ -66,7 +69,6 @@ "ඔබ මුල් පිටුවට යාමේ ඉංගිතය සම්පූර්ණ කළා" "මුල් පිටුවට යාමට ස්වයිප් කරන්න" "ඔබගේ තිරයේ පහළින් උඩට ස්වයිප් කරන්න.මෙම ඉංගිතය සැම විටම ඔබව මුල් තිරයට ගෙන යයි." - "තිරයේ පහළම සිට ඇඟිලි 2කින් ඉහළට ස්වයිප් කරන්න. මෙම ඉංගිතය සැම විටම ඔබව මුල් තිරයට ගෙන යයි." "මුල් පිටුවට යන්න" "ඔබේ තිරයේ පහළ සිට උඩට ස්වයිප් කරන්න" "අනර්ඝ වැඩක්!" @@ -77,7 +79,6 @@ "ඔබ යෙදුම් මාරු කිරීමේ ඉංගිතය සම්පූර්ණ කළා" "යෙදුම් මාරු කිරීමට ස්වයිප් කරන්න" "යෙදුම් අතර මාරු වීමට, ඔබගේ තිරයේ පහළම සිට උඩට ස්වයිප් කර, අල්ලාගෙන සිට, අනතුරුව මුදා හරින්න." - "යෙදුම් අතර මාරු වීමට, ඔබගේ තිරයේ පහළම සිට උඩට ඇඟිලි 2කින් ස්වයිප් කර, අල්ලාගෙන සිට, අනතුරුව මුදා හරින්න." "යෙදුම් මාරු කරන්න" "ඔබේ තිරයේ පහළ සිට ඉහළට ස්වයිප් කරන්න, රඳවා ගෙන සිට, පසුව මුදා හරින්න" "හොඳින් කළා!" @@ -91,24 +92,33 @@ "මුල් පිටුවට යාමට ඉහළට ස්වයිප් කරන්න" "ඔබේ මුල් තිරය වෙත යාමට මුල් පිටුව බොත්තම තට්ටු කරන්න" "ඔබ ඔබේ %1$s භාවිත කිරීම පටන් ගැනීමට සූදානම්" - "උපාංගය" + "ඔබ ඔබේ උපාංගය භාවිත කිරීම පටන් ගැනීමට සූදානම්" "පද්ධති සංචාලන සැකසීම්" + "ඔබේ %1$s \nසූදානම්!" + "ඔබේ \n සූදානම්!" + "ඔබේ අලුත් %1$s රසවිඳින්න!" + "සංචාලනය කරන ආකාරය තෝරන්න" + "ඉහළට ස්වයිප් කරන්න" + "මුල් බොත්තම තට්ටු කරන්න" + "ටැබ්ලටය" + "දුරකථනය" "බෙදා ගන්න" "තිර රුව" "බෙදන්න" "යෙදුම් යුගල සුරකින්න" "බෙදුම් තිරය භාවිතා කිරීමට තවත් යෙදුමක් තට්ටු කරන්න" "බෙදුම් තිරය භාවිත කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" - "අවලංගු කරන්න" + "අවලංගු කරන්න" "බෙදීම් තිර තේරීමෙන් පිටවන්න" "බෙදීම් තිරය භාවිතා කිරීමට වෙනත් යෙදුමක් තෝරා ගන්න" "මෙම ක්‍රියාව යෙදුම හෝ ඔබේ සංවිධානය මගින් ඉඩ නොදේ" "විජට් දැනට සහාය නොදක්වයි, වෙනත් යෙදුමක් තෝරන්න" - "නිබන්ධනය සංචාලනය මඟ හරින්නද?" - "ඔබට මෙය පසුව %1$s යෙදුම තුළ සොයා ගත හැකිය" - "අවලංගු කරන්න" - "මඟ හරින්න" "තිරය කරකවන්න" + "තිරයේ පහළ සිට කාර්ය තීරුව දර්ශනය වන ආකාරය සහ භාවිතයේ නොමැති විට ස්වයංක්‍රීයව සැඟවෙන ආකාරය පෙන්වන සජීවීකරණය" + "තිරයේ පහළින් කාර්ය තීරුව ස්ථිරවම දෘශ්‍යමානව පවතින පරිදි, ටොගලයක් භාවිතයෙන් ඔබේ කාර්ය තීරුව ඇමිණෙන ආකාරය පෙන්වන සජීවීකරණය" + "විවෘත යෙදුමකට ඉහළින් ඇති කාර්ය තීරුවෙන් යෙදුමක් ඇදගෙන යාමෙන්, බෙදීම් තිරයක් නිර්මාණය කරන ආකාරය පෙන්වන සජීවීකරණය" + "ඔබේ උපාංගයේ යෝජිත යෙදුම් වෙත ප්‍රවේශ වන ආකාරය පෙන්වන සජීවීකරණය" + "ක්‍රියා යතුර ස්පර්ශ කරගෙන අයිතමය ඇති ප්‍රදේශය තේරීමෙන් තිරය මත අයිතමයක් සොයන ආකාරය පෙන්වන සජීවීකරණය" "කාර්ය තීරු අධ්‍යාපනය" "එකවර යෙදුම් 2ක් භාවිතා කිරීමට යෙදුමක් පැත්තට අදින්න" "කාර්ය තීරුව පෙන්වීමට ඉහළට සෙමින් ස්වයිප් කරන්න" @@ -130,18 +140,40 @@ "ඉක්මන් සැකසීම්" "කාර්ය තීරුව" "කාර්ය තීරුව පෙන්වා ඇත" - "කාර්ය තීරුව සඟවා ඇත" + "කාර්ය තීරුව සහ බුබුළු පෙන්වා ඇත" + "කාර්ය තීරුව සහ බුබුළු දකුණට පෙන්වා ඇත" "සංචලන තීරුව" "සෑම විටම කාර්ය තීරුව පෙන්වන්න" "සංචාලන ප්‍රකාරය වෙනස් කරන්න" "කාර්ය තීරු බෙදනය" + "අනෙකුත් මෑත යෙදුම්" "ඉහළ/වම වෙත ගෙන යන්න" "පහළ/දකුණ වෙත ගෙන යන්න" - "{count,plural, =1{තවත් # යෙදුමක් පෙන්වන්න.}one{තවත් යෙදුම් #ක් පෙන්වන්න.}other{තවත් යෙදුම් #ක් පෙන්වන්න.}}" - "{count,plural, =1{# ඩෙස්ක්ටොප් යෙදුමක් පෙන්වන්න.}one{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}other{ඩෙස්ක්ටොප් යෙදුම් # ක් පෙන්වන්න.}}" + "යෙදුම බුබුලක් ලෙස විවෘත කරන්න" + "මෑත යෙදුම්" + "මෑත යෙදුම් ලැයිස්තුව" + "{count,plural, =1{තව යෙදුම}one{තවත් යෙදුම්}other{තවත් යෙදුම්}}" + "ඩෙස්ක්ටොපය" "%1$s සහ %2$s" + "%1$s, අයිතම %3$dන් %2$d" + "වමට අනුචලනය කරන්න" + "දකුණට අනුචලනය කරන්න" "බුබුළු" "පිටාර යාම" "%2$s සිට %1$s" "%1$s හා තව %2$dක්" + "වමට ගෙන යන්න" + "දකුණට ගෙන යන්න" + "සියල්ල ඉවතලන්න" + "%1$s දිග හරින්න" + "%1$s හකුළන්න" + "සෙවීමට කවයසෙවීමට කවය අදින්න" + "යෙදුම් නිරූපකය" + "යෙදුම් මාතෘකාව" + "වැසීමේ බොත්තම" + "කාර්ය තීරුවට අමුණන්න" + "කාර්ය තීරුවෙන් ඉවත් කරන්න" + "සෙමින් පෙළඹවීම" + "වසන්න" + "රූපය සෙමින් පෙළඹවීම" diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml index 3eca787fed..e5c99a54ec 100644 --- a/quickstep/res/values-sk/strings.xml +++ b/quickstep/res/values-sk/strings.xml @@ -22,11 +22,14 @@ "Pripnúť" "Voľný režim" "Počítač" + "Presunúť na externú obrazovku" + "Vymazať" + "Počítač" "Žiadne nedávne položky" "Nastavenia využívania aplikácie" "Vymazať všetko" + "Pridať novú plochu" "Nedávne aplikácie" - "Úloha bola zavretá" "%1$s, %2$s" "Menej ako 1 minúta" "Dnes ešte zostáva: %1$s" @@ -45,41 +48,39 @@ "Návrhy aplikácií zapnuté" "Návrhy aplikácií vypnuté" "Predpovedaná aplikácia: %1$s" + "Návod na navigáciu gestami" "Otočte zariadenie" "Otočte zariadenie a dokončite tak návod, ako navigovať gestami" - "Musíte potiahnuť úplne z pravého alebo ľavého okraja" + "Musíte potiahnuť úplne z pravého alebo ľavého okraja." "Musíte potiahnuť z pravého alebo ľavého okraja do stredu obrazovky a potom uvoľniť" "Naučili ste sa prejsť späť potiahnutím sprava. V ďalšom kroku sa naučíte prepínať aplikácie." "Dokončili ste gesto na prechod späť. V ďalšom kroku sa naučíte, ako prepínať aplikácie." - "Dokončili ste gesto na prechod späť" + "Použili ste gesto na prechod späť." "Nesmiete potiahnuť príliš blízko dolnej časti obrazovky" "Ak chcete zmeniť citlivosť gesta Späť, prejdite do Nastavení" "Prechod späť potiahnutím" "Na poslednú obrazovku prejdete potiahnutím z ľavého alebo pravého okraja do stredu obrazovky." - "Na poslednú obrazovku sa vrátite potiahnutím dvoma prstami z ľavého alebo pravého okraja do stredu obrazovky." "Prechod späť" - "Potiahnite z ľavého alebo pravého okraja do stredu obrazovky" - "Musíte potiahnuť nahor z dolného okraja obrazovky" + "Potiahnite z ľavého alebo pravého okraja do stredu obrazovky." + "Musíte potiahnuť nahor z dolného okraja obrazovky." "Pred uvoľnením nesmiete zastať" "Musíte potiahnuť priamo nahor" "Dokončili ste gesto prechodu na plochu. Teraz sa naučíte, ako sa vrátiť späť." "Dokončili ste gesto prechodu na plochu" "Prechod na plochu potiahnutím" "Potiahnite nahor zdola obrazovky. Týmto gestom sa vždy vrátite na plochu." - "Postiahnite dvoma prstami z dolnej časti obrazovky. Týmto gestom sa vždy vrátite na plochu." "Prechod na plochu" - "Potiahnite z dolnej časti obrazovky nahor" + "Potiahnite z dolnej časti obrazovky nahor." "Skvelé!" "Musíte potiahnuť nahor z dolného okraja obrazovky" "Skúste okno pred uvoľnením podržať dlhšie" - "Musite potiahnuť priamo nahor a potom zastať" - "Naučili ste sa používať gestá. Gestá môžete vypnúť v nastaveniach." - "Dokončili ste gesto na prepnutie aplikácií" + "Musite potiahnuť priamo nahor a potom zastať." + "Naučili ste sa používať gestá. Gestá môžete vypnúť v Nastaveniach." + "Použili ste gesto na prepnutie aplikácií." "Prepínanie aplikácií potiahnutím" "Aplikácie môžete prepínať potiahnutím obrazovky zdola nahor, pridržaním a následným uvoľnením." - "Aplikácie prepnete potiahnutím dvoma prstami z dolnej časti obrazovky, ich pridržaním a uvoľnením." "Prepnutie aplikácií" - "Potiahnite nahor z dolného okraja obrazovky, pridržte a uvoľnite" + "Potiahnite nahor z dolného okraja obrazovky, pridržte a uvoľnite." "Výborne" "Hotovo" "Hotovo" @@ -90,34 +91,43 @@ "Hotovo" "Potiahnutím nahor prejdete na plochu" "Na plochu prejdete klepnutím na tlačidlo plochy" - "%1$s môžete začať používať" - "zariadenie" + "Môžete %1$s začať používať" + "Zariadenie môžete začať používať" "Nastavenia navigácie systémom" + "Vaše zariadenie %1$s je \npripravené." + "Vaše zariadenie je \npripravené." + "Užite si svoj nový %1$s!" + "Vyberte spôsob prechádzania" + "Potiahnite nahor" + "Klepnite na tlačidlo plochy" + "tablet" + "telefón" "Zdieľať" "Snímka obrazovky" "Rozdeliť" "Uložiť pár aplikácií" "Obrazovku rozdelíte klepnutím na inú aplikáciu" "Na použitie rozdelenej obrazovky vyberte ďalšiu aplikáciu" - "Zrušiť" + "Zrušiť" "Ukončite výber rozdelenej obrazovky" "Na použitie rozd. obrazovky vyberte inú aplikáciu" "Aplikácia alebo vaša organizácia túto akciu nepovoľuje" "Miniaplikácie nie sú momentálne podporované, vyberte inú aplikáciu" - "Chcete preskočiť návod na navigáciu?" - "Tento návod nájdete v aplikácii %1$s" - "Zrušiť" - "Preskočiť" "Otočiť obrazovku" + "Animácia zobrazujúca, ako sa panel aplikácií zobrazí z dolnej časti obrazovky a automaticky skryje, keď ho nepoužívate" + "Animácia znázorňujúca, ako pripnúť panel aplikácií pomocou prepínača, aby bol stále viditeľný v dolnej časti obrazovky" + "Animácia znázorňujúca, ako vytvoriť rozdelenú obrazovku presunutím aplikácie z panela aplikácií nad otvorenú aplikáciu" + "Animácia znázorňujúca, ako získať prístup k navrhovaným aplikáciám v zariadení" + "Animácia znázorňujúca, ako vyhľadať určitú položku na obrazovke pridržaním akčného klávesa a výberom oblasti, v ktorej sa daná položka nachádza" "Panel vzdelávacích aplikácií" - "Ak chcete použiť dve aplikácie naraz, presuňte aplikáciu nabok" + "Ak chcete používať dve aplikácie naraz, presuňte aplikáciu nabok" "Panel aplikácií zobrazíte pomalým potiahnutím nahor" "Získavajte návrhy aplikácií na základe svojich zvykov" - "Dlhým stlačením rozdeľovača pripnete panel aplikácií" + "Dlhým stlačením rozdeľovača panel aplikácií pripnete" "Panel aplikácií vám ponúka ďalšie možnosti" "Vždy zobrazovať panel aplikácií" "Ak chcete, aby sa panel aplikácií vždy zobrazoval v dolnej časti obrazovky, pridržte rozdeľovač" - "Ak chcete vyhľadávať, čo je na obrazovke, pridržte akčný kláves" + "Pridržaním akčného klávesu môžete vyhľadávať, čo je na obrazovke" "Táto služba používa na účely vyhľadávania vybranú časť obrazovky. Uplatňujú sa <a href="%1$s">pravidlá ochrany súkromia</a><a href="%2$s">zmluvné podmienky</a> spoločnosti Google." "Zavrieť" "Hotovo" @@ -130,18 +140,40 @@ "Rýchle nastavenia" "Panel aplikácií" "Panel aplikácií je zobrazený" - "Panel aplikácií je skrytý" + "Panel aplik. a bubl. sú vľavo" + "Panel aplik. a bubl. sú vpravo" "Navigačný panel" "Zobrazovať panel aplikácií" "Zmeniť režim navigácie" "Rozdeľovač panela aplikácií" + "Iné nedávne aplikácie" "Presunúť hore alebo doľava" "Presunúť dole alebo doprava" - "{count,plural, =1{Zobraziť # ďalšiu aplikáciu.}few{Zobraziť # ďalšie aplikácie.}many{Show # more apps.}other{Zobraziť # ďalších aplikácií.}}" - "{count,plural, =1{Zobraziť # aplikáciu pre počítač.}few{Zobraziť # aplikácie pre počítač.}many{Show # desktop apps.}other{Zobraziť # aplikácií pre počítač.}}" + "Otvoriť aplikáciu ako bublinu" + "Nedávne aplikácie" + "Zoznam nedávnych aplikácií" + "{count,plural, =1{ďalšia aplikácia}few{ďalšie aplikácie}many{ďalšie aplikácie}other{ďalšie aplikácie}}" + "Počítač" "%1$s%2$s" + "%1$s, %2$d. položka z %3$d" + "Posunúť doľava" + "Posunúť doprava" "Bublina" "Rozbaľovacia ponuka" "%1$s z aplikácie %2$s" "%1$s a ešte %2$d" + "Posunúť doľava" + "Posunúť doprava" + "Zavrieť všetko" + "rozbaliť %1$s" + "zbaliť %1$s" + "Hľadanie krúžkovaním" + "Ikona aplikácie" + "Názov aplikácie" + "Tlačidlo Zavrieť" + "Pripnúť na panel" + "Odopnúť z panela" + "Posunúť" + "Zavrieť" + "Posunúť obrázok" diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml index 52faeb7a9e..440def5794 100644 --- a/quickstep/res/values-sl/strings.xml +++ b/quickstep/res/values-sl/strings.xml @@ -22,11 +22,14 @@ "Pripni" "Prosta oblika" "Namizni računalnik" + "Premik v zunanji zaslon" + "Počisti" + "Namizni način" "Ni nedavnih elementov" "Nastavitve uporabe aplikacij" "Počisti vse" + "Dodajanje novega namizja" "Nedavne aplikacije" - "Opravilo je zaprto" "%1$s, %2$s" "< 1 min" "Danes je ostalo še %1$s" @@ -45,6 +48,7 @@ "Predlogi aplikacij so omogočeni." "Predlogi aplikacij so onemogočeni." "Predvidena aplikacija: %1$s" + "Vadnica za krmarjenje s potezami" "Zasukajte napravo" "Zasukajte napravo, če si želite ogledati vadnico za krmarjenje s potezami" "Pazite, da povlečete s skrajno desnega ali skrajno levega roba." @@ -56,7 +60,6 @@ "Občutljivost poteze za nazaj lahko spremenite v nastavitvah." "Povlecite za vrnitev" "Če se želite vrniti na prejšnji zaslon, povlecite z levega ali desnega roba do sredine zaslona." - "Če se želite vrniti na zadnji prikazani zaslon, z dvema prstoma povlecite z levega ali desnega roba do sredine zaslona." "Pomik nazaj" "Povlecite z levega ali desnega roba do sredine zaslona." "Pazite, da povlečete s spodnjega roba zaslona navzgor." @@ -66,7 +69,6 @@ "Izvedli ste potezo za pomik na začetni zaslon." "Povlecite za pomik na začetni zaslon" "Z dna zaslona s prstom povlecite navzgor. S to potezo lahko vedno odprete začetni zaslon." - "Z dvema prstoma povlecite navzgor z dna zaslona. S to potezo lahko vedno odprete začetni zaslon." "Pomik na začetni zaslon" "Z dna zaslona s prstom povlecite navzgor." "Odlično!" @@ -77,7 +79,6 @@ "Izvedli ste potezo za preklapljanje med aplikacijami." "Povlecite za preklapljanje med aplikacijami" "Za preklapljanje med aplikacijami povlecite navzgor z dna zaslona, pridržite in nato izpustite." - "Za preklop med aplikacijami z dvema prstoma povlecite navzgor z dna zaslona, pridržite in spustite." "Preklop aplikacij" "Povlecite navzgor z dna zaslona, pridržite, nato izpustite." "Odlično!" @@ -91,24 +92,33 @@ "Povlecite navzgor za začetni zaslon" "Za pomik na začetni zaslon se dotaknite gumba za začetni zaslon." "Pripravljeni ste, da začnete uporabljati %1$s" - "napravo" + "Pripravljeni ste, da začnete uporabljati napravo" "Nastavitve krmarjenja po sistemu" + "Naprava %1$s je \npripravljena" + "Naprava je \npripravljena" + "Uživajte v novi napravi %1$s" + "Izbira načina pomikanja" + "Povlecite navzgor" + "Dotaknite se gumba za začetni zaslon" + "tablični računalnik" + "telefon" "Deli" "Posnetek zaslona" "Razdeli" "Shrani par aplikacij" "Za razdeljeni zaslon se dotaknite še 1 aplikacije" "Izberite drugo aplikacijo za uporabo razdeljenega zaslona." - "Prekliči" + "Prekliči" "Zapri izbiro razdeljenega zaslona" "Izberite drugo aplikacijo za uporabo razdeljenega zaslona." "Aplikacija ali vaša organizacija ne dovoljuje tega dejanja" "Pripomočki trenutno niso podprti, izberite drugo aplikacijo" - "Želite preskočiti vadnico za krmarjenje?" - "To lahko pozneje najdete v aplikaciji %1$s." - "Prekliči" - "Preskoči" "Sukanje zaslona" + "Animacija, ki prikazuje, kako se opravilna vrstica prikaže na dnu zaslona in se samodejno skrije, ko je ne uporabljate" + "Animacija, ki prikazuje, kako s preklopnikom pripnete opravilno vrstico, da bo vedno vidna na dnu zaslona" + "Animacija, ki prikazuje, kako ustvarite razdeljen zaslon tako, da aplikacijo iz opravilne vrstice povlečete in spustite na odprto aplikacijo" + "Animacija, ki prikazuje, kako dostopate do predlaganih aplikacij v napravi" + "Animacija, ki prikazuje, kako na zaslonu poiščete element tako, da se dotaknete in pridržite tipko za dejanja ter izberete območje, kjer je element" "Poučni nasveti o opravilni vrstici" "Povlecite aplikacijo na stran za uporabo 2 aplikacij hkrati." "Počasi povlecite navzgor za prikaz opravilne vrstice" @@ -117,7 +127,7 @@ "Naredite več z opravilno vrstico" "Stalni prikaz opravilne vrstice" "Če želite, da je opravilna vrstica vedno prikazana na dnu zaslona, pridržite razdelilno črto." - "Za iskanje po zaslonu se dotaknite in pridržite tipko za dejanja" + "Za iskanje vsebine zaslona pridržite tipko za dejanja" "Ta izdelek za iskanje uporablja izbrani del zaslona. Veljajo Googlov <a href="%1$s">pravilnik o zasebnosti</a> in <a href="%2$s">pogoji storitve</a>." "Zapri" "Končano" @@ -130,18 +140,40 @@ "Hitre nastavitve" "Opravilna vrstica" "Opravilna vrstica je prikazana" - "Opravilna vrstica je skrita" + "Prikazani so opravilna vrstica in oblački na levi" + "Prikazani so opravilna vrstica in oblački na desni" "Vrstica za krmarjenje" "Stalen prikaz oprav. vrstice" "Spreminjanje načina navigacije" "Razdelilnik opravilne vrstice" + "Druge nedavne aplikacije" "Premakni na vrh/levo" "Premakni na dno/desno" - "{count,plural, =1{Pokaži še # aplikacijo.}one{Pokaži še # aplikacijo.}two{Pokaži še # aplikaciji.}few{Pokaži še # aplikacije.}other{Pokaži še # aplikacij.}}" - "{count,plural, =1{Prikaz # aplikacije za namizni računalnik.}one{Prikaz # aplikacije za namizni računalnik.}two{Prikaz # aplikacij za namizni računalnik.}few{Prikaz # aplikacij za namizni računalnik.}other{Prikaz # aplikacij za namizni računalnik.}}" + "Odpri aplikacijo kot oblaček" + "Nedavne aplikacije" + "Seznam nedavnih aplikacij" + "{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}two{dodatni aplikaciji}few{dodatne aplikacije}other{dodatnih aplikacij}}" + "Namizni računalnik" "%1$s in %2$s" + "%1$s, element %2$d od %3$d" + "Pomik levo" + "Pomik desno" "Oblaček" "Oblaček z dodatnimi elementi" "%1$s iz aplikacije %2$s" "%1$s in še %2$d" + "Premik v levo" + "Premik v desno" + "Opusti vse" + "razširitev oblačka %1$s" + "strnitev oblačka %1$s" + "Iskanje z obkroževanjem" + "Ikona aplikacije" + "Ime aplikacije" + "Gumb za zapiranje" + "Pripni v opravilno vrstico" + "Odpni iz opravilne vrstice" + "Dreganje" + "Zapri" + "Dregni sliko" diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml index cdb9cf9a7c..58961bc221 100644 --- a/quickstep/res/values-sq/strings.xml +++ b/quickstep/res/values-sq/strings.xml @@ -22,11 +22,14 @@ "Gozhdo" "Formë e lirë" "Desktopi" + "Zhvendose tek ekrani i jashtëm" + "Pastro" + "Desktopi" "Nuk ka asnjë artikull të fundit" "Cilësimet e përdorimit të aplikacionit" "Pastroji të gjitha" + "Shto një tavolinë të re pune" "Aplikacionet e fundit" - "Detyra u mbyll" "%1$s, %2$s" "< 1 minutë" "%1$s të mbetura sot" @@ -45,6 +48,7 @@ "Aplikacionet e sugjeruara janë aktivizuar" "Sugjerimet e aplikacioneve janë çaktivizuar" "Aplikacioni i parashikuar: %1$s" + "Udhëzuesi për navigimin me gjeste" "Rrotullo pajisjen" "Rrotullo pajisjen për të përfunduar udhëzuesin e navigimit me gjeste" "Sigurohu që të rrëshqasësh shpejt nga skaji më i djathtë ose më i majtë" @@ -56,7 +60,6 @@ "Për të ndryshuar ndjeshmërinë e gjestit të kthimit prapa, shko te \"Cilësimet\"" "Rrëshqit shpejt për t\'u kthyer prapa" "Për t\'u kthyer prapa tek ekrani i fundit, rrëshqit shpejt nga skaji i majtë ose i djathtë drejt mesit të ekranit" - "Për t\'u kthyer prapa tek ekrani i fundit, rrëshqit shpejt me 2 gishta nga skaji i majtë ose i djathtë drejt mesit të ekranit." "Kthehu prapa" "Rrëshqit shpejt nga skaji i majtë ose i djathtë drejt mesit të ekranit" "Sigurohu që të rrëshqasësh shpejt lart nga skaji i poshtëm i ekranit" @@ -66,7 +69,6 @@ "E ke përfunduar gjestin e kalimit tek ekrani bazë" "Rrëshqit shpejt për të kaluar tek ekrani bazë" "Rrëshqit shpejt lart nga fundi i ekranit tënd. Ky gjest të dërgon gjithmonë tek ekrani bazë." - "Rrëshqit shpejt lart me 2 gishta nga fundi i ekranit. Ky gjest të dërgon gjithmonë tek ekrani bazë." "Shko tek ekrani bazë" "Rrëshqit shpejt lart nga pjesa e poshtme e ekranit" "Punë e shkëlqyer!" @@ -77,7 +79,6 @@ "E ke përfunduar gjestin e ndërrimit të aplikacioneve" "Rrëshqit shpejt për të ndërruar aplikacionet" "Për të ndërruar mes aplikacioneve, rrëshqit shpejt lart nga fundi i ekranit tënd, mbaj dhe pastaj lësho." - "Për të ndërruar mes aplikacioneve, rrëshqit lart me 2 gishta nga fundi i ekranit, mbaje dhe lëshoje." "Ndërro aplikacionet" "Rrëshqit shpejt lart nga fundi i ekranit, mbaje të shtypur dhe më pas lëshoje" "Shumë mirë!" @@ -91,24 +92,33 @@ "Rrëshqit shpejt lart për të shkuar në ekranin bazë" "Trokit te butoni \"kreu\" për të shkuar tek ekrani bazë" "Je gati që të fillosh ta përdorësh këtë %1$s" - "pajisje" + "Je gati që të fillosh të përdorësh pajisjen tënde" "Cilësimet e navigimit të sistemit" + "Pajisja jote %1$s është \ngati!" + "Pajisja jote është \ngati!" + "Shijoje %1$s tënd të ri!" + "Zgjidh se si do të navigosh" + "Rrëshqit shpejt lart" + "Trokit te butoni \"kreu\"" + "tabletin" + "telefonin" "Ndaj" "Pamja e ekranit" "Ndaj" "Ruaj çiftin e aplikacioneve" "Trokit një apl. tjetër; përdor ekranin e ndarë" "Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë" - "Anulo" + "Anulo" "Dil nga zgjedhja e ekranit të ndarë" "Zgjidh një aplikacion tjetër për të përdorur ekranin e ndarë" "Ky veprim nuk lejohet nga aplikacioni ose organizata jote" "Miniaplikacionet nuk mbështeten për momentin. Zgjidh një aplikacion tjetër" - "Të kapërcehet udhëzuesi i navigimit?" - "Këtë mund ta gjesh më vonë tek aplikacioni \"%1$s\"" - "Anulo" - "Kapërce" "Rrotullo ekranin" + "Animacioni që tregon se si shfaqet shiriti i detyrave nga fundi i ekranit dhe si fshihet automatikisht kur nuk është në përdorim" + "Animacioni që tregon se si të gozhdosh shiritin e detyrave duke përdorur një çelës, në mënyrë që shiriti i detyrave të qëndrojë përgjithmonë i dukshëm në fund të ekranit" + "Animacioni që tregon si të krijosh një ekran të ndarë duke zvarritur dhe lëshuar një aplikacion nga shiriti i detyrave mbi një aplikacion të hapur" + "Animacioni që tregon se si të qasesh në aplikacionet e sugjeruara në pajisjen tënde" + "Animacioni që tregon se si të kërkosh për një artikull në ekran, duke prekur dhe mbajtur shtypur tastin e veprimit dhe duke zgjedhur zonën ku është artikulli" "Edukimi për shiritin e detyrave" "Zvarrit një aplikacion në anë për të përdorur 2 aplikacione njëherësh" "Rrëshqit lart ngadalë për të shfaqur \"Shiritin e detyrave\"" @@ -130,18 +140,40 @@ "Cilësimet shpejt" "Shiriti i detyrave" "Shiriti i detyrave i shfaqur" - "Shiriti i detyrave i fshehur" + "Shiriti i detyrave dhe flluskat majtas janë shfaqur" + "Shiriti i detyrave dhe flluskat djathtas janë shfaqur" "Shiriti i navigimit" "Shfaq gjithmonë shiritin e detyrave" "Ndrysho modalitetin e navigimit" "Ndarësi i shiritit të detyrave" + "Aplikacionet e tjera së fundi" "Lëviz në krye/majtas" "Lëviz në fund/djathtas" - "{count,plural, =1{Shfaq # aplikacion tjetër.}other{Shfaq # aplikacione të tjera.}}" - "{count,plural, =1{Shfaq # aplikacion për desktop.}other{Shfaq # aplikacione për desktop.}}" + "Hap aplikacionin si një flluskë" + "Aplikacionet e fundit" + "Lista e aplikacioneve të fundit" + "{count,plural, =1{aplikacion tjetër}other{aplikacione të tjera}}" + "Desktop" "%1$s dhe %2$s" + "%1$s, artikulli %2$d nga %3$d" + "Lëviz majtas" + "Lëviz djathtas" "Flluska" "Tejkalimi" "\"%1$s\" nga %2$s" "\"%1$s\" dhe %2$d të tjera" + "Lëviz majtas" + "Lëviz djathtas" + "Hiqi të gjitha" + "zgjero %1$s" + "palos %1$s" + "Qarko për të kërkuar" + "Ikona e aplikacionit" + "Titulli i aplikacionit" + "Butoni i mbylljes" + "Gozhdo te shiriti" + "Zhgozhdo nga shiriti" + "Shtytje" + "Mbyll" + "Shtyj imazhin" diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml index 7456a36b7e..baf6032dc6 100644 --- a/quickstep/res/values-sr/strings.xml +++ b/quickstep/res/values-sr/strings.xml @@ -22,11 +22,14 @@ "Закачи" "Слободни облик" "Рачунар" + "Преместите на спољни екран" + "Обриши" + "Рачунари" "Нема недавних ставки" "Подешавања коришћења апликације" "Обриши све" + "Додај нову радну површину" "Недавне апликације" - "Задатак је затворен" "%1$s, %2$s" "< 1 мин" "Још %1$s данас" @@ -45,18 +48,18 @@ "Предлози апликација су омогућени" "Предлози апликација су онемогућени" "Предвиђамо апликацију: %1$s" + "Водич за навигацију помоћу покрета" "Ротирајте уређај" "Ротирајте уређај да бисте довршили водич за навигацију помоћу покрета" "Обавезно превуците од саме десне или леве ивице" "Обавезно превуците од десне или леве ивице до средине екрана и отпустите" "Научили сте како да превлачите здесна да бисте се вратили уназад. Сада научите да замените апликације." "Довршили сте покрет за повратак. Сада сазнајте како да промените апликације." - "Довршили сте покрет за повратак" + "Довршили сте покрет за назад" "Никако не превлачите превише близу дна екрана" "Осетљивост пок. за назад можете да промените у Подешавањима" "Превуците да бисте се вратили уназад" "Да бисте се вратили на последњи екран, превуците од леве или десне ивице до средине екрана." - "Да бисте се вратили на последњи екран, превуците помоћу два прста од леве или десне ивице до средине екрана." "Назад" "Превуците од леве или десне ивице до средине екрана" "Обавезно превуците нагоре од доње ивице екрана" @@ -66,8 +69,7 @@ "Довршили сте покрет за повратак на почетну страницу." "Превуците да бисте отишли на почетну страницу" "Превуците нагоре од дна екрана. Овај покрет вас увек води на почетни екран." - "Превуците помоћу два прста нагоре од дна екрана. Овим покретом увек отварате почетни екран." - "Идите на почетни екран" + "На почетни екран" "Превуците нагоре са доњег дела екрана" "Одлично!" "Обавезно превуците нагоре од доње ивице екрана" @@ -77,8 +79,7 @@ "Довршили сте покрет за промену апликација" "Превуците да бисте заменили апликације" "За прелазак са једне апликације на другу превуците нагоре од дна екрана, задржите, па пустите." - "За прелазак између апликација превуците помоћу два прста нагоре од дна екрана, задржите, па пустите." - "Пређите на другу апликацију" + "На другу апликацију" "Превуците нагоре од дна екрана, задржите, па пустите" "Одлично!" "То је то" @@ -89,26 +90,35 @@ "Водич %1$d/%2$d" "Готово!" "Превуците нагоре да бисте отворили почетни екран" - "Додирните дугме Почетак да бисти ишли на почетни екран" + "Додирните дугме Почетак да бисте отишли на почетни екран" "Спремни сте да почнете да користите %1$s" - "уређај" + "Спремни сте да почнете да користите уређај" "Подешавања кретања кроз систем" + "%1$s је \nспреман!" + "Уређај је \nспреман!" + "Уживајте уз нови %1$s!" + "Изаберите подешавања навигације" + "Превуците нагоре" + "Додирните дугме Почетак" + "таблет" + "телефон" "Дели" "Снимак екрана" "Подели" "Сачувај пар апликација" "Додирните другу апликацију за подељени екран" "Одаберите другу апликацију да бисте користили подељени екран" - "Откажи" + "Откажи" "Излазак из бирања подељеног екрана" "Одаберите другу апликацију за подељени екран" "Апликација или организација не дозвољавају ову радњу" "Виџети тренутно нису подржани. Изаберите другу апликацију" - "Желите да прескочите водич за кретање?" - "Можете да пронађете ово касније у апликацији %1$s" - "Откажи" - "Прескочи" "Ротирајте екран" + "Анимација која показује како се трака задатака приказује при дну екрана и аутоматски скрива када се не користи" + "Анимација која показује како да помоћу прекидача закачите траку задатака тако да увек буде видљива у дну екрана" + "Анимација која показује како да направите подељени екран превлачењем и отпуштањем апликације са траке задатака изнад отворене апликације" + "Анимација која показује како да приступате предложеним апликацијама на уређају" + "Анимација која показује како да потражите ставку на екрану тако што ћете додирнути и задржати тастер радњи и изабрати област у којој се налази ова ставка" "Упутства на траци задатака" "Превуците на страну да бисте користили 2 апликације одједном" "Споро превуците нагоре да бисте видели траку задатака" @@ -130,18 +140,40 @@ "Брза подешавања" "Трака задатака" "Трака задатака је приказана" - "Трака задатака је скривена" + "Приказ задатака/облачића лево" + "Приказ задатака/облачића десно" "Трака за навигацију" "Увек приказуј траку задатака" "Промени режим навигације" "Разделник траке задатака" + "Друге недавне апликације" "Премести горе лево" "Премести доле десно" - "{count,plural, =1{Прикажи још # апликацију.}one{Прикажи још # апликацију.}few{Прикажи још # апликације.}other{Прикажи још # апликација.}}" - "{count,plural, =1{Прикажи # апликацију за рачунаре.}one{Прикажи # апликацију за рачунаре.}few{Прикажи # апликације за рачунаре.}other{Прикажи # апликација за рачунаре.}}" + "Отвори апликацију као облачић" + "Недавне апликације" + "Листа недавних апликација" + "{count,plural, =1{додатна апликација}one{додатна апликација}few{додатне апликације}other{додатних апликација}}" + "Рачунар" "%1$s и %2$s" + "%1$s, ставка %2$d од %3$d" + "Скролујте улево" + "Скролујте удесно" "Облачић" "Преклопни" "%1$s%2$s" "%1$s и још %2$d" + "Помери налево" + "Помери надесно" + "Одбаци све" + "проширите облачић %1$s" + "скупите облачић %1$s" + "Претрага заокруживањем" + "Икона апликације" + "Назив апликације" + "Дугме Затвори" + "Закачи за траку зад." + "Откачи са траке зад." + "Аутоматски подсетник" + "Затворите" + "Слика аутоматског подсетника" diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml index f369daeb33..303d2f04fd 100644 --- a/quickstep/res/values-sv/strings.xml +++ b/quickstep/res/values-sv/strings.xml @@ -21,12 +21,15 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Fäst" "Fritt format" - "Dator" + "Skrivbordsläge" + "Flytta till extern skärm" + "Rensa" + "Skrivbordsläge" "Listan är tom" "Inställningar för appanvändning" "Rensa alla" + "Lägg till nytt skrivbord" "Senaste apparna" - "Uppgiften har stängts" "%1$s, %2$s" "< 1 min" "%1$s kvar i dag" @@ -45,6 +48,7 @@ "Appförslag har aktiverats" "Appförslag har inaktiverats" "Appförslag: %1$s" + "Guide för navigering med rörelser" "Rotera enheten" "Rotera enheten för att slutföra guiden för navigering med rörelser" "Se till att du sveper ända från högerkanten eller vänsterkanten" @@ -56,7 +60,6 @@ "Öppna inställningarna om du vill ändra rörelsens känslighet" "Svep för att återgå" "Återgå till den senaste skärmen genom att svepa från skärmens vänster- eller högerkant till mitten." - "Gå tillbaka till den senaste skärmen genom att med två fingrar svepa mot mitten av skärmen från vänster eller höger kant." "Tillbaka" "Svep från den högra eller vänstra kanten till mitten av skärmen" "Se till att du sveper uppåt från nederkanten av skärmen" @@ -66,7 +69,6 @@ "Du är klar med rörelsen för att öppna startskärmen" "Svep för att öppna startskärmen" "Svep uppåt från skärmens nederkant. Du kan alltid återgå till startskärmen med den här rörelsen." - "Svep uppåt med två fingrar från skärmens nederkant. Så kommer du alltid tillbaka till startskärmen." "Öppna startskärmen" "Svep uppåt från skärmens nederkant" "Bra jobbat!" @@ -77,7 +79,6 @@ "Du är klar med rörelsen för att byta mellan appar" "Svep för att byta mellan appar" "Byt mellan appar genom att svepa uppåt från skärmens nederkant. Håll fingret nedtryckt och släpp." - "Byta mellan appar: Svep uppåt med två fingrar från skärmens nederkant, håll kvar och släpp sedan." "Byt app" "Svep uppåt från skärmens nederkant. Håll fingret nedtryckt och släpp sedan" "Bra gjort!" @@ -91,24 +92,33 @@ "Svep uppåt för att öppna startskärmen" "Tryck på hemknappen för att öppna startskärmen" "Nu kan du börja använda din %1$s" - "enhet" + "Nu kan du börja använda din enhet" "Systemnavigeringsinställningar" + "Din %1$s är \nklar!" + "Din enhet är \nklar!" + "Ha kul med din nya %1$s!" + "Välj hur du vill navigera" + "Svep uppåt" + "Tryck på hemknappen" + "surfplatta" + "telefon" "Dela" "Skärmbild" "Delad skärm" "Spara app-par" "Tryck på en annan app för att använda delad skärm" "Välj en annan app för att använda delad skärm" - "Avbryt" + "Avbryt" "Avsluta val av delad skärm" "Välj en annan app för att använda delad skärm" "Appen eller organisationen tillåter inte den här åtgärden" "Widgetar stöds för närvarande inte. Välj en annan app" - "Vill du hoppa över självstudierna?" - "Du hittar det här igen i %1$s-appen" - "Avbryt" - "Hoppa över" "Rotera skärmen" + "Animation som visar hur aktivitetsfältet dyker upp längst ned på skärmen och automatiskt döljs när det inte används" + "Animation som visar hur du fäster aktivitetsfältet med ett reglage så att det alltid är synligt längst ned på skärmen" + "Animation som visar hur du skapar en delad skärm genom att dra och släppa en app från aktivitetsfältet ovanför en öppen app" + "Animation som visar hur du kommer åt föreslagna appar på enheten" + "Animation som visar hur du söker efter ett objekt på skärmen genom att trycka länge på åtgärdstangenten och välja området där objektet finns" "Aktivitetsfältsutbildning" "Dra en app till sidan om du vill använda två appar samtidigt" "Svep långsamt uppåt för att visa aktivitetsfältet" @@ -130,18 +140,40 @@ "Snabbinställn." "Aktivitetsfält" "Aktivitetsfältet visas" - "Aktivitetsfältet är dolt" + "Vänster fält och bubblor visas" + "Höger fält och bubblor visas" "Navigeringsfält" "Visa alltid aktivitetsfältet" "Ändra navigeringsläge" "Avdelare för aktivitetsfältet" + "Andra appar som använts nyligen" "Flytta högst upp/till vänster" "Flytta längst ned/till höger" - "{count,plural, =1{Visa # app till.}other{Visa # appar till.}}" - "{count,plural, =1{Visa # datorapp.}other{Visa # datorappar.}}" + "Öppna appen som en bubbla" + "Senaste apparna" + "Lista över senaste appar" + "{count,plural, =1{app till}other{appar till}}" + "Skrivbordsläge" "%1$s och %2$s" + "%1$s, objekt %2$d av %3$d" + "Scrolla åt vänster" + "Scrolla åt höger" "Bubbla" "Fler alternativ" "%1$s från %2$s" "%1$s och %2$d till" + "Flytta åt vänster" + "Flytta åt höger" + "Stäng alla" + "utöka %1$s" + "komprimera %1$s" + "Circle to Search" + "Appikon" + "Apptitel" + "Knappen Stäng" + "Fäst i aktivitetsfält" + "Lossa från aktivitetsfält" + "Hint" + "Stäng" + "Bild i hint" diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml index 3d8277b448..115e10d988 100644 --- a/quickstep/res/values-sw/strings.xml +++ b/quickstep/res/values-sw/strings.xml @@ -22,11 +22,14 @@ "Bandika" "Muundo huru" "Kompyuta ya mezani" + "Hamishia programu kwenye skrini ya nje" + "Futa" + "Kompyuta ya Mezani" "Hakuna vipengee vya hivi karibuni" "Mipangilio ya matumizi ya programu" "Ondoa zote" + "Weka eneokazi jipya" "Programu za hivi karibuni" - "Jukumu Limefungwa" "%1$s, %2$s" "< dak 1" "Umebakisha %1$s leo" @@ -45,28 +48,27 @@ "Mapendekezo ya programu yamewashwa" "Umezima mapendekezo ya programu" "Programu iliyotabiriwa: %1$s" + "Mafunzo ya Usogezaji kwa Kutumia Miguso" "Zungusha kifaa chako" - "Tafadhali zungusha kifaa chako ili ukamilishe mafunzo ya usogezaji kwa kutumia ishara" + "Tafadhali zungusha kifaa chako ili ukamilishe mafunzo ya usogezaji kwa kutumia miguso" "Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto kabisa" "Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto hadi katikati ya skrini na uachilie" "Umejifunza jinsi ya kutelezesha kidole kuanzia kulia ili kurudi nyuma. Sasa jifunze jinsi ya kubadilisha programu." - "Umekamilisha ishara ya kurudi nyuma. Hatua inayofuata, jifunze jinsi ya kubadilisha programu." - "Umeweka ishara ya kurudi nyuma" + "Umekamilisha mafunzo ya miguso ya kurudi nyuma. Hatua inayofuata, fahamu jinsi ya kubadilisha programu." + "Umekamilisha mafunzo ya miguso ya kurudi nyuma" "Hakikisha hutelezeshi kidole karibu sana na sehemu ya chini ya skrini" "Kubadilisha hisi ya ishara ya nyuma, nenda kwenye Mipangilio" "Telezesha kidole ili urudi nyuma" "Ili urudi kwenye skrini iliyotangulia, telezesha kidole kuanzia ukingo wa kushoto au wa kulia kuelekea katikati ya skrini." - "Ili urudi kwenye skrini iliyopita, telezesha vidole viwili kuanzia ukingo wa kushoto au wa kulia kuelekea katikati ya skrini." "Rudi nyuma" "Telezesha kidole kutoka ukingo wa kushoto au kulia hadi katikati ya skrini" "Hakikisha unatelezesha kidole juu kuanzia ukingo wa chini wa skrini" "Hakikisha husitishi kabla ya kuachilia" "Hakikisha unatelezesha kidole juu" - "Umeweka ishara ya kwenda kwenye Skrini ya kwanza. Inayofuata, jifunze jinsi ya kurudi nyuma." - "Umeweka ishara ya kwenda kwenye skrini ya kwanza" + "Umekamilisha mguso wa kwenda kwenye skrini ya kwanza. Inayofuata, fahamu jinsi ya kurudi nyuma." + "Umekamilisha mguso wa kwenda kwenye skrini ya kwanza" "Telezesha kidole ili uende kwenye skrini ya kwanza" "Telezesha kidole juu kuanzia chini ya skrini yako. Ishara hii kila wakati hukupeleka kwenye Skrini ya kwanza." - "Telezesha vidole viwili kuelekea juu kuanzia sehemu ya chini ya skrini. Ishara hii kila wakati hukupeleka kwenye Skrini ya kwanza." "Nenda kwenye ukurasa wa mwanzo" "Telezesha kidole juu kutoka sehemu ya chini ya skrini yako" "Kazi nzuri!" @@ -74,10 +76,9 @@ "Jaribu kushikilia dirisha kwa muda mrefu kabla ya kuachilia" "Hakikisha unatelezesha kidole juu, kisha usitishe" "Umejifunza jinsi ya kutumia ishara. Ili uzime ishara, nenda kwenye Mipangilio." - "Umeweka ishara ya kubadilisha programu" + "Umekamilisha mguso wa kubadilisha programu" "Telezesha kidole ili ubadilishe programu" "Ili ubadili kati ya programu, telezesha kidole juu kuanzia sehemu ya chini ya skrini yako, ushikilie, kisha uachilie." - "Ili ubadilishe kati ya programu, telezesha vidole viwili kuelekea juu kuanzia sehemu ya chini ya skrini yako, ushikilie, kisha uachilie." "Badilisha programu" "Telezesha kidole juu kutoka sehemu ya chini ya skrini yako, shikilia kisha uachilie" "Hongera!" @@ -91,24 +92,33 @@ "Telezesha kidole juu ili uende kwenye skrini ya kwanza" "Gusa kitufe cha ukurasa wa mwanzo ili uende kwenye skrini ya kwanza" "Uko tayari kuanza kutumia %1$s" - "kifaa" + "Uko tayari kuanza kutumia kifaa chako" "Mipangilio ya usogezaji kwenye mfumo" + "%1$s yako iko \ntayari!" + "Kifaa chako kiko \ntayari!" + "Furahia %1$s yako mpya!" + "Chagua jinsi ya kusogeza" + "Telezesha kidole juu" + "Gusa kitufe cha ukurasa wa mwanzo" + "kishikwambi" + "simu" "Shiriki" "Picha ya skrini" "Iliyogawanywa" "Hifadhi jozi ya programu" "Gusa programu nyingine ili utumie kipengele cha kugawa skrini" "Chagua programu nyingine ili utumie hali ya kugawa skrini" - "Ghairi" + "Acha" "Ondoka kwenye hali ya skrini iliyogawanywa" "Chagua programu nyingine ili utumie hali ya kugawa skrini" "Kitendo hiki hakiruhusiwi na programu au shirika lako" "Wijeti hazitumiwi kwa sasa, tafadhali chagua programu nyingine" - "Ungependa kuruka mafunzo ya usogezaji?" - "Utapata mafunzo haya baadaye katika programu ya %1$s" - "Ghairi" - "Ruka" "Zungusha skrini" + "Uhuishaji unaoonyesha jinsi upauzana huonekana kutokea sehemu ya chini ya skrini na kufichwa kiotomatiki wakati hautumiki" + "Uhuishaji unaoonyesha jinsi ya kubandika upauzana wako kwa kutumia kipengele cha kuwasha/kuzima, ili upauzana uendelee kuonekana sehemu ya chini kabisa ya skrini" + "Uhuishaji unaoonyesha jinsi ya kuweka skrini iliyogawanywa, kwa kuburuta na kudondosha programu kutoka kwenye upauzana ulio juu ya programu iliyofunguliwa" + "Uhuishaji unaoonyesha jinsi ya kufikia programu zinazopendekezwa kwenye kifaa chako" + "Uhuishaji unaoonyesha jinsi ya kutafuta bidhaa kwenye skrini, kwa kugusa na kushikilia kitufe cha vitendo kisha kuchagua eneo liliko bidhaa" "Elimu ya Upauzana" "Buruta programu pembeni ili utumie programu 2 kwa wakati mmoja" "Telezesha kidole juu taratibu ili ufungue Upauzana" @@ -130,18 +140,40 @@ "Mipangilio ya Haraka" "Upauzana" "Upauzana umeonyeshwa" - "Upauzana umefichwa" + "Upauzana na viputo vinaonyeshwa kushoto" + "Upauzana na viputo vinaonyeshwa kulia" "Sehemu ya viungo muhimu" "Onyesha Zana kila wakati" "Badilisha hali ya usogezaji" "Kitenganishi cha Upauzana" + "Programu nyingine za hivi majuzi" "Sogeza juu/kushoto" "Sogeza chini/kulia" - "{count,plural, =1{Onyesha programu # zaidi.}other{Onyesha programu # zaidi.}}" - "{count,plural, =1{Onyesha programu # ya kompyuta ya mezani.}other{Onyesha programu # za kompyuta ya mezani.}}" + "Fungua programu kama kiputo" + "Programu ulizofungua hivi majuzi" + "Orodha ya programu ulizofungua hivi majuzi" + "{count,plural, =1{programu nyingine}other{programu zingine}}" + "Kompyuta ya Mezani" "%1$s na %2$s" + "%1$s, kipengee cha %2$d kati ya %3$d" + "Sogeza kushoto" + "Sogeza kulia" "Kiputo" "Kiputo cha vipengee vya ziada" "%1$s kutoka %2$s" "%1$s na vingine %2$d" + "Sogeza kushoto" + "Sogeza kulia" + "Ondoa vyote" + "panua %1$s" + "kunja %1$s" + "Chora Mviringo ili Kutafuta" + "Aikoni ya programu" + "Kichwa cha programu" + "Kitufe cha kufunga" + "Bandika kwa upauzana" + "Bandua kwa upauzana" + "Dokezo" + "Funga" + "Picha ya dokezo" diff --git a/quickstep/res/values-sw600dp-land/dimens.xml b/quickstep/res/values-sw600dp-land/dimens.xml index 5e9a177b8b..cf7ba0094b 100644 --- a/quickstep/res/values-sw600dp-land/dimens.xml +++ b/quickstep/res/values-sw600dp-land/dimens.xml @@ -16,7 +16,7 @@ --> - 48dp + 48dp 48dp @@ -27,10 +27,6 @@ 40dp 49dp - - - 24dp - - 40dp + 24dp diff --git a/quickstep/res/values-sw600dp/config.xml b/quickstep/res/values-sw600dp/config.xml index b22cfc5118..34d16955dc 100644 --- a/quickstep/res/values-sw600dp/config.xml +++ b/quickstep/res/values-sw600dp/config.xml @@ -17,4 +17,8 @@ 8 + + + 0.775 + 2400 diff --git a/quickstep/res/values-sw600dp/dimens.xml b/quickstep/res/values-sw600dp/dimens.xml index e24d8fea79..253c9643f6 100644 --- a/quickstep/res/values-sw600dp/dimens.xml +++ b/quickstep/res/values-sw600dp/dimens.xml @@ -33,20 +33,12 @@ 36dp 64dp - - 80dp - - 80dp 24dp - 120dp + 120dp 38sp 15sp - - - - 300dp - + 16sp diff --git a/quickstep/res/values-sw720dp/dimens.xml b/quickstep/res/values-sw720dp/dimens.xml index 1caffb8a72..dbe82f277b 100644 --- a/quickstep/res/values-sw720dp/dimens.xml +++ b/quickstep/res/values-sw720dp/dimens.xml @@ -37,6 +37,7 @@ 42sp 16sp + 16sp 30dp diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml index 47d8055e16..9a752ed23c 100644 --- a/quickstep/res/values-ta/strings.xml +++ b/quickstep/res/values-ta/strings.xml @@ -22,11 +22,14 @@ "பின் செய்தல்" "குறிப்பிட்ட வடிவமில்லாத பயன்முறை" "டெஸ்க்டாப்" + "வெளிப்புற டிஸ்ப்ளேவிற்கு நகர்த்துதல்" + "அழிக்கும்" + "டெஸ்க்டாப்" "சமீபத்தியவை எதுவுமில்லை" "ஆப்ஸ் உபயோக அமைப்புகள்" "எல்லாம் அழி" + "புதிய டெஸ்க்கைச் சேர்க்கும்" "சமீபத்திய ஆப்ஸ்" - "பணி முடிந்தது" "%1$s, %2$s" "< 1 நி" "இன்று %1$s மீதமுள்ளது" @@ -45,6 +48,7 @@ "ஆப்ஸ் பரிந்துரைகள் இயக்கப்பட்டுள்ளன" "ஆப்ஸ் பரிந்துரைகள் முடக்கப்பட்டுள்ளன" "கணித்த ஆப்ஸ்: %1$s" + "சைகை வழிசெலுத்தலுக்கான பயிற்சி" "உங்கள் சாதனத்தைச் சுழற்றுங்கள்" "சைகை வழிசெலுத்தல் பயிற்சியை நிறைவுசெய்ய உங்கள் சாதனத்தைச் சுழற்றுங்கள்" "வலது அல்லது இடது ஓரத்தின் விளிம்பிலிருந்து ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்" @@ -56,7 +60,6 @@ "பின்செல் சைகையின் உணர்திறனை மாற்ற அமைப்புகளுக்குச் செல்க" "பின்செல்ல ஸ்வைப் செய்யுங்கள்" "முந்தைய திரைக்கு மீண்டும் செல்ல, இடது/வலது ஓரத்திலிருந்து திரையின் மையப் பகுதிக்கு ஸ்வைப் செய்க." - "முந்தைய திரைக்கு மீண்டும் செல்ல, 2 விரல்களால் இடது அல்லது வலது ஓரத்திலிருந்து திரையின் மையப் பகுதிக்கு ஸ்வைப் செய்யுங்கள்." "பின்செல்லுதல்" "வலது அல்லது இடது ஓரத்திலிருந்து திரையின் மையப் பகுதிக்கு ஸ்வைப் செய்யுங்கள்" "திரையின் கீழ் ஓரத்திலிருந்து மேல்நோக்கி ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்" @@ -66,18 +69,16 @@ "முகப்புக்குச் செல் சைகைப் பயிற்சியை நிறைவுசெய்துவிட்டீர்கள்" "முகப்புக்குச் செல்ல ஸ்வைப் செய்யுங்கள்" "திரையின் கீழிருந்து மேலாக ஸ்வைப் செய்க. இந்தச் சைகை எப்போதும் முகப்புத் திரைக்கு அழைத்துச் செல்லும்." - "2 விரலால் திரையின் கீழிருந்து மேலாக ஸ்வைப் செய்க. இந்தச் சைகை முகப்புத் திரைக்கு அழைத்துச் செல்லும்." "முகப்புக்குச் செல்லுதல்" "திரையின் கீழிருந்து மேல்நோக்கி ஸ்வைப் செய்யுங்கள்" "அருமை!" "திரையின் கீழ் ஓரத்திலிருந்து மேல்நோக்கி ஸ்வைப் செய்வதை உறுதிசெய்துகொள்ளுங்கள்" "விடுவிப்பதற்கு முன்பாக நீண்டநேரம் சாளரத்தை அழுத்திப் பிடித்திருங்கள்" - "மேல்நோக்கி நேராக ஸ்வைப் செய்தபிறகு இடைநிறுத்துவதை உறுதிசெய்துகொள்ளுங்கள்" + "மேல்நோக்கி நேராக ஸ்வைப் செய்தபிறகு சற்றுநேரம் அழுத்திபடியே வைத்திருங்கள்" "சைகைகளை எப்படி உபயோகிப்பது என்று கற்றுக்கொண்டீர்கள். சைகைகளை முடக்க அமைப்புகளுக்குச் செல்லுங்கள்." "ஆப்ஸுக்கிடையே மாறும் சைகைப் பயிற்சியை நிறைவுசெய்துவிட்டீர்கள்" "ஆப்ஸுக்கிடையே மாற ஸ்வைப் செய்யுங்கள்" "ஆப்ஸுக்கு இடையே மாற, திரையின் கீழிலிருந்து மேலாக ஸ்வைப் செய்து, பிடித்திருந்து, பிறகு விடுவிக்கவும்." - "ஆப்ஸுக்கிடையே மாற, திரையின் கீழிருந்து மேலாக 2 விரலால் ஸ்வைப் செய்து, பிடித்து, பிறகு விடுவிக்கவும்." "ஆப்ஸுக்கிடையே மாறுதல்" "உங்கள் திரையின் கீழ்ப்பகுதியில் இருந்து மேலே ஸ்வைப் செய்து, பிடித்து, பிறகு விடுவியுங்கள்" "அருமை!" @@ -90,25 +91,34 @@ "அனைத்தையும் அமைத்துவிட்டீர்கள்!" "முகப்புக்குச் செல்ல மேல்நோக்கி ஸ்வைப் செய்யுங்கள்" "முகப்புத் திரைக்குச் செல்வதற்கு முகப்பு பட்டனைத் தட்டவும்" - "உங்கள் %1$s சாதனத்தைப் பயன்படுத்தத் தயாராகிவிட்டீர்கள்" - "சாதனம்" + "உங்கள் %1$s உங்களுக்காகத் தயாராக இருக்கிறது" + "உங்கள் சாதனத்தைப் பயன்படுத்தத் தயாராகிவிட்டீர்கள்" "சிஸ்டம் வழிசெலுத்தல் அமைப்புகள்" + "உங்கள் %1$s \nதயார்!" + "உங்கள் சாதனம் \nதயார்!" + "உங்கள் புதிய %1$s சாதனத்தைப் பயன்படுத்தி மகிழுங்கள்!" + "வழிசெலுத்துவதற்கான வழிமுறையைத் தேர்வுசெய்யுங்கள்" + "மேல்நோக்கி ஸ்வைப் செய்யுங்கள்" + "முகப்பு பட்டனைத் தட்டுங்கள்" + "டேப்லெட்" + "மொபைல்" "பகிர்" "ஸ்கிரீன்ஷாட்" "பிரி" "ஆப்ஸ் ஜோடியைச் சேமி" "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தட்டவும்" "திரைப் பிரிப்பைப் பயன்படுத்த வேறு ஆப்ஸைத் தேர்வுசெய்யுங்கள்" - "ரத்துசெய்" + "ரத்துசெய்" "திரைப் பிரிப்பு தேர்வில் இருந்து வெளியேறும்" "திரைப் பிரிப்பை பயன்படுத்த வேறு ஆப்ஸை தேர்வுசெய்க" "ஆப்ஸோ உங்கள் நிறுவனமோ இந்த செயலை அனுமதிப்பதில்லை" "விட்ஜெட்கள் தற்போது ஆதரிக்கப்படவில்லை, வேறு ஆப்ஸைத் தேர்ந்தெடுக்கவும்" - "வழிகாட்டுதல் பயிற்சியைத் தவிர்க்கவா?" - "%1$s ஆப்ஸில் பிறகு இதைக் கண்டறியலாம்" - "ரத்துசெய்" - "தவிர்" "திரையைச் சுழற்றும்" + "திரையின் அடிப்பகுதியில் இருந்து செயல் பட்டி எப்படிக் காட்டப்படுகிறது, பயன்பாட்டில் இல்லாதபோது தானாகவே மறைகிறது என்பதைக் காட்டும் அனிமேஷன்" + "நிலைமாற்றியைப் பயன்படுத்தி உங்கள் செயல் பட்டியை எப்படி பின் செய்வது என்பதைக் காட்டும் அனிமேஷன், இதனால் திரையின் அடிப்பகுதியில் செயல் பட்டி நிரந்தரமாகக் காட்டப்படும்" + "திறந்திருக்கும் ஆப்ஸின் மேலேயுள்ள செயல் பட்டியிலிருந்து ஒரு ஆப்ஸை இழுத்து விடுவதன் மூலம், திரைப் பிரிப்பை எப்படி உருவாக்குவது என்பதைக் காட்டும் அனிமேஷன்" + "உங்கள் சாதனத்தில் பரிந்துரைக்கப்பட்ட ஆப்ஸை அணுகுவது எப்படி என்பதைக் காட்டும் அனிமேஷன்" + "ஆக்‌ஷன் பட்டனைத் தொட்டுப் பிடித்து, ஆவணம் இருக்கும் பகுதியைத் தேர்ந்தெடுப்பதன் மூலம் திரையில் ஒன்றைத் தேடுவது எப்படி என்பதைக் காட்டும் அனிமேஷன்" "செயல் பட்டியைப் பயன்படுத்தும் விதம்" "ஆப்ஸை பக்கவாட்டில் இழுத்து ஒரே நேரத்தில் 2 ஆப்ஸைப் பயன்படுத்தலாம்" "செயல் பட்டியைக் காட்ட மேல்நோக்கி மெதுவாக ஸ்வைப் செய்யவும்" @@ -130,18 +140,40 @@ "விரைவு அமைப்புகள்" "செயல் பட்டி" "செயல் பட்டி காட்டப்படுகிறது" - "செயல் பட்டி மறைக்கப்பட்டுள்ளது" + "செயல் பட்டி & குமிழை இடதுபுறம் காட்டும்" + "செயல் பட்டி & குமிழை வலதுபுறம் காட்டும்" "வழிசெலுத்தல் பட்டி" "செயல் பட்டியை எப்போதும் காட்டு" "வழிசெலுத்தல் பயன்முறையை மாற்று" "செயல் பட்டிப் பிரிப்பான்" + "பிற சமீபத்திய ஆப்ஸ்" "மேலே/இடதுபுறம் நகர்த்தும்" "கீழே/வலதுபுறம் நகர்த்தும்" - "{count,plural, =1{மேலும் # ஆப்ஸைக் காட்டு.}other{மேலும் # ஆப்ஸைக் காட்டு.}}" - "{count,plural, =1{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}other{# டெஸ்க்டாப் ஆப்ஸைக் காட்டு.}}" + "ஆப்ஸைக் குமிழாகத் திற" + "சமீபத்திய ஆப்ஸ்" + "சமீபத்திய ஆப்ஸ் பட்டியல்" + "{count,plural, =1{கூடுதல் ஆப்ஸ்}other{கூடுதல் ஆப்ஸ்}}" + "டெஸ்க்டாப்" "%1$s மற்றும் %2$s" + "%1$s, %3$d இல் %2$d கட்டம்" + "இடதுபுறம் நகர்த்தும்" + "வலதுபுறம் நகர்த்தும்" "குமிழ்" "கூடுதல் விருப்பங்களைக் காட்டும்" "%2$s வழங்கும் %1$s" "%1$s மற்றும் %2$d" + "இடதுபுறம் நகர்த்தும்" + "வலதுபுறம் நகர்த்தும்" + "அனைத்தையும் மூடும்" + "%1$s ஐ விரிவாக்கும்" + "%1$s ஐச் சுருக்கும்" + "வட்டமிட்டுத் தேடல்" + "ஆப்ஸ் ஐகான்" + "ஆப்ஸ் தலைப்பு" + "மூடுவதற்கான பட்டன்" + "செயல்பட்டியில் பின் செய்" + "செயல்பட்டியிலிருந்து அகற்று" + "நினைவூட்டல்" + "மூடும்" + "படம் குறித்து நினைவூட்டும்" diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml index a4e1cbf09c..e27a7272d5 100644 --- a/quickstep/res/values-te/strings.xml +++ b/quickstep/res/values-te/strings.xml @@ -22,11 +22,14 @@ "పిన్ చేయండి" "సంప్రదాయేతర" "డెస్క్‌టాప్" + "ఎక్స్‌టర్నల్ డిస్‌ప్లేకు తరలించండి" + "క్లియర్ చేయండి" + "డెస్క్‌టాప్" "ఇటీవలి ఐటెమ్‌లు ఏవీ లేవు" "యాప్ వినియోగ సెట్టింగ్‌లు" - "అన్నీ తీసివేయండి" + "అన్నీ క్లియర్ చేయండి" + "కొత్త డెస్క్‌ను జోడించండి" "ఇటీవలి యాప్‌లు" - "టాస్క్ మూసివేయబడింది" "%1$s, %2$s" "< 1 నిమిషం" "నేటికి %1$s మిగిలి ఉంది" @@ -45,6 +48,7 @@ "యాప్ సలహాలు ఎనేబుల్ చేయబడ్డాయి" "యాప్ సూచ‌న‌లు డిజేబుల్‌ చేయబడ్డాయి" "సూచించబడిన యాప్: %1$s" + "సంజ్ఞ నావిగేషన్ ట్యుటోరియల్" "మీ పరికరాన్ని రొటేట్ చేయండి" "సంజ్ఞ నావిగేషన్ ట్యుటోరియల్‌ను పూర్తి చేయడానికి దయచేసి మీ పరికరాన్ని రొటేట్ చేయండి" "కుడి వైపు చిట్ట చివరి లేదా ఎడమ వైపు చిట్ట చివరి అంచు నుండి స్వైప్ చేస్తున్నారని నిర్ధారించుకోండి" @@ -56,7 +60,6 @@ "వెనుక సంజ్ఞ సున్నితత్వం మార్చడానికి, సెట్టింగ్‌లకు వెళ్లండి" "వెనుకకు వెళ్ళడం కోసం స్వైప్ చేయండి" "మునుపటి స్క్రీన్‌కు తిరిగి వెళ్లడానికి, ఎడమ లేదా కుడి అంచు నుండి స్క్రీన్ మధ్యలోకి స్వయిప్ చేయండి." - "గత స్క్రీన్‌కు తిరిగి వెళ్లడానికి, ఎడమ లేదా కుడి అంచు నుండి స్క్రీన్ మధ్యలోకి 2 వేళ్లతో స్వైప్ చేయండి." "వెనుకకు వెళ్లండి" "ఎడమ లేదా కుడి అంచు నుండి స్క్రీన్ మధ్యకు స్వైప్ చేయండి" "మీరు స్క్రీన్ దిగువ అంచు నుండి పైకి స్వైప్ చేశారని నిర్ధారించుకోండి" @@ -66,18 +69,16 @@ "మీరు మొదటి స్క్రీన్‌కు వెళ్లే సంజ్ఞను పూర్తి చేశారు" "మొదటి స్క్రీన్‌కు వెళ్లడానికి స్వైప్ చేయండి" "స్క్రీన్ కింది నుండి పైకి స్వైప్ చేయండి. ఈ సంజ్ఞ ఎప్పుడూ మిమ్మల్ని మొదటి స్క్రీన్‌కు తీసుకెళ్తుంది." - "స్క్రీన్ కింది నుండి 2 వేళ్లతో పైకి స్వైప్ చేయండి. సంజ్ఞ ఎల్లప్పుడూ మొదటి స్క్రీన్‌కు తీసుకెళ్తుంది." - "మొదటి ట్యాబ్‌కు వెళ్లండి" + "హోం స్క్రీన్‌కు వెళ్లండి" "స్క్రీన్ కింది భాగం నుండి పైకి స్వైప్ చేయండి" "బాగా చేశారు!" "మీరు స్క్రీన్ దిగువ అంచు నుండి పైకి స్వైప్ చేశారని నిర్ధారించుకోండి" "వేలిని రిలీజ్ చేయడానికి ముందు విండోను ఎక్కువసేపు నొక్కి, పట్టుకోవడానికి ట్రై చేయండి" "స్క్రీన్‌పై నేరుగా పైకి స్వైప్ చేసి, ఆపై పాజ్ చేయండి" "మీరు సంజ్ఞలను ఎలా ఉపయోగించాలో నేర్చుకున్నారు. సంజ్ఞలను ఆఫ్ చేయడానికి, సెట్టింగ్‌లకు వెళ్లండి." - "మీరు \'యాప్‌ల మధ్య మార్పు\' సంజ్ఞను పూర్తి చేశారు" + "మీరు \'యాప్‌ల మధ్య మారేందుకు సంజ్ఞ\' ట్యుటోరియల్‌ను పూర్తి చేశారు" "యాప్‌ల మధ్య మార్చడం కోసం స్వైప్ చేయండి" "యాప్‌ల మధ్య మారడానికి, మీ స్క్రీన్ కింది వైపు నుండి పైకి స్వైప్ చేసి, పట్టుకుని, తర్వాత వదలండి." - "యాప్‌ల మధ్య మారడానికి, మీ స్క్రీన్ కింది నుండి 2 వేళ్లతో పైకి స్వైప్ చేసి, నొక్కి పట్టి, వదలండి." "యాప్‌ల మధ్య స్విచ్ అవ్వండి" "మీ స్క్రీన్ కింది వైపు నుండి పైకి స్వైప్ చేసి, పట్టుకుని, తర్వాత వదలండి" "చాలా బాగా చేశారు!" @@ -91,24 +92,33 @@ "వర్చువల్ హోమ్‌కు వెళ్లడానికి పైకి స్వైప్ చేయండి" "మీ మొదటి స్క్రీన్‌కు వెళ్లడానికి హోమ్ బటన్‌ను ట్యాప్ చేయండి" "మీరు ఇప్పుడు మీ %1$s‌ను ఉపయోగించడం ప్రారంభించవచ్చు" - "పరికరం" + "మీరు ఇప్పుడు మీ డివైజ్‌ను ఉపయోగించడానికి సిద్ధంగా ఉన్నారు" "సిస్టమ్ నావిగేషన్ సెట్టింగ్‌లు" + "మీ %1$s సిద్ధంగా \nఉంది!" + "మీ డివైజ్ సిద్ధంగా \nఉంది!" + "మీ కొత్త %1$s‌ను ఉపయోగిస్తూ ఎంజాయ్ చేయండి!" + "ఎలా నావిగేట్ చేయాలో ఎంచుకోండి" + "పైకి స్వైప్ చేయండి" + "హోమ్ బటన్‌ను ట్యాప్ చేయండి" + "టాబ్లెట్" + "ఫోన్" "షేర్ చేయండి" "స్క్రీన్‌షాట్" "స్ప్లిట్ చేయండి" "యాప్ పెయిర్‌ను సేవ్ చేయండి" "స్ప్లిట్ స్క్రీన్ కోసం మరొక యాప్‌ను ట్యాప్ చేయండి" "స్ప్లిట్ స్క్రీన్‌ను ఉపయోగించడానికి మరొక యాప్ ఎంచుకోండి" - "రద్దు చేయండి" + "రద్దు చేయండి" "స్ప్లిట్ స్క్రీన్ ఎంపిక నుండి ఎగ్జిట్ అవ్వండి" "స్ప్లిట్ స్క్రీన్ ఉపయోగానికి మరొక యాప్ ఎంచుకోండి" "ఈ చర్యను యాప్ గానీ, మీ సంస్థ గానీ అనుమతించవు" "విడ్జెట్‌లకు ప్రస్తుతం సపోర్ట్ లేదు, దయచేసి మరొక యాప్‌ను ఎంచుకోండి" - "నావిగేషన్ ట్యుటోరియల్‌ను స్కిప్ చేయాలా?" - "%1$s యాప్‌లో మీరు తర్వాత కనుగొనవచ్చు" - "రద్దు చేయండి" - "స్కిప్ చేయండి" "స్క్రీన్‌ను తిప్పండి" + "టాస్క్‌బార్ స్క్రీన్ దిగువ నుండి ఎలా కనిపిస్తుందో, ఉపయోగంలో లేనప్పుడు ఆటోమేటిక్‌గా ఎలా దాచబడుతుందో చూపించే యానిమేషన్" + "టోగుల్ ఉపయోగించి మీ టాస్క్‌బార్‌ను ఎలా పిన్ చేయాలో చూపించే యానిమేషన్, తద్వారా టాస్క్‌బార్ స్క్రీన్ దిగువున శాశ్వతంగా కనిపిస్తుంది" + "తెరిచి ఉన్న యాప్ పైన టాస్క్‌బార్ నుండి యాప్‌ను లాగి, వదలడం ద్వారా స్ప్లిట్ స్క్రీన్‌ను ఎలా క్రియేట్ చేయాలో చూపించే యానిమేషన్" + "మీ పరికరంలో సూచించబడిన యాప్‌లను ఎలా యాక్సెస్ చేయాలో చూపించే యానిమేషన్" + "యాక్షన్ కీని నొక్కి పట్టుకుని, ఆ ఐటెమ్ ఉన్న ప్రాంతాన్ని ఎంచుకోవడం ద్వారా స్క్రీన్‌పై ఐటెమ్ కోసం ఎలా సెర్చ్ చేయాలో చూపించే యానిమేషన్" "టాస్క్‌బార్ ఎడ్యుకేషన్" "ఒకేసారి 2 యాప్‌లను ఉపయోగించడానికి యాప్‌ను పక్కకు లాగండి" "టాస్క్‌బార్‌ను చూపడానికి నెమ్మదిగా పైకి స్వైప్ చేయండి" @@ -130,18 +140,40 @@ "క్విక్ సెట్టింగ్‌లు" "టాస్క్‌బార్" "టాస్క్‌బార్ చూపబడింది" - "టాస్క్‌బార్ దాచబడింది" + "టాస్క్‌బార్, బబుల్స్ ఎడమవైపున చూపబడ్డాయి" + "టాస్క్‌బార్, బబుల్స్ కుడివైపున చూపబడ్డాయి" "నావిగేషన్ బార్" "టాస్క్‌బార్‌ను నిరంతరం చూపండి" "నావిగేషన్ మోడ్‌ను మార్చండి" "టాస్క్‌బార్ డివైడర్" + "ఇతర ఇటీవలి యాప్‌లు" "ఎగువ/ఎడమ వైపునకు తరలించండి" "దిగువ/కుడి వైపునకు తరలించండి" - "{count,plural, =1{మరో # యాప్‌ను చూడండి.}other{మరో # యాప్‌లను చూడండి.}}" - "{count,plural, =1{# డెస్క్‌టాప్ యాప్‌ను చూపండి.}other{# డెస్క్‌టాప్ యాప్‌లను చూపండి.}}" + "యాప్‌ను బబుల్‌లాగా తెరవండి" + "ఇటీవలి యాప్‌లు" + "ఇటీవలి యాప్ లిస్ట్" + "{count,plural, =1{మరో యాప్‌}other{మరిన్ని యాప్‌లు}}" + "డెస్క్‌టాప్" "%1$s, %2$s" + "%1$s, %3$d‌లో %2$d‌వ ఐటెమ్" + "ఎడమవైపునకు స్క్రోల్ చేయండి" + "కుడివైపునకు స్క్రోల్ చేయండి" "బబుల్" "ఓవర్‌ఫ్లో" "%2$s నుండి %1$s" "%1$s, మరో %2$d" + "ఎడమ వైపుగా జరపండి" + "కుడి వైపుగా జరపండి" + "అన్నింటినీ విస్మరించండి" + "%1$sను విస్తరించండి" + "%1$sను కుదించండి" + "సెర్చ్ చేయడానికి సర్కిల్ గీయండి" + "యాప్ చిహ్నం" + "యాప్ టైటిల్" + "\'మూసివేయండి\' బటన్" + "టాస్క్‌బార్‌కు పిన్" + "టాస్క్‌బార్ అన్‌పిన్" + "ఆటోమేటిక్ రిమైండర్" + "మూసివేయండి" + "ఆటోమేటిక్ రిమైండర్ ఇమేజ్" diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml index 1bbb137a1b..4d41065b54 100644 --- a/quickstep/res/values-th/strings.xml +++ b/quickstep/res/values-th/strings.xml @@ -22,11 +22,14 @@ "ปักหมุด" "รูปแบบอิสระ" "เดสก์ท็อป" + "ย้ายไปยังจอแสดงผลภายนอก" + "ล้าง" + "เดสก์ท็อป" "ไม่มีรายการล่าสุด" "การตั้งค่าการใช้แอป" "ล้างทั้งหมด" + "เพิ่มเดสก์ใหม่" "แอปล่าสุด" - "ปิดงานแล้ว" "%1$s %2$s" "<1 นาที" "วันนี้เหลืออีก %1$s" @@ -45,39 +48,37 @@ "เปิดใช้แอปแนะนำแล้ว" "ปิดใช้คำแนะนำเกี่ยวกับแอปอยู่" "แอปที่คาดว่าจะใช้: %1$s" + "บทแนะนำการไปยังส่วนต่างๆ ด้วยท่าทางสัมผัส" "หมุนอุปกรณ์ของคุณ" - "โปรดหมุนอุปกรณ์เพื่อทำตามบทแนะนำการนำทางด้วยท่าทางสัมผัสให้เสร็จสมบูรณ์" - "ตรวจสอบว่าปัดจากขอบด้านขวาสุดหรือซ้ายสุด" + "โปรดหมุนอุปกรณ์เพื่อทำตามบทแนะนำการไปยังส่วนต่างๆ ด้วยท่าทางสัมผัสให้เสร็จสมบูรณ์" + "ปัดจากขอบด้านขวาสุดหรือซ้ายสุด" "ตรวจสอบว่าปัดจากขอบด้านขวาหรือซ้ายไปตรงกลางหน้าจอ แล้วยกนิ้วขึ้น" "คุณรู้วิธีปัดจากด้านขวาเพื่อย้อนกลับแล้ว ต่อไปดูวิธีสลับแอป" - "คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว ต่อไปดูวิธีสลับแอป" - "คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว" + "คุณทำท่าทางสัมผัสเพื่อย้อนกลับสำเร็จแล้ว ต่อไปดูวิธีสลับแอป" + "คุณทำท่าทางสัมผัสเพื่อย้อนกลับสำเร็จแล้ว" "ไม่ปัดใกล้กับด้านล่างของหน้าจอมากเกินไป" "เปลี่ยนความไวของท่าทางสัมผัสเพื่อย้อนกลับได้ที่การตั้งค่า" "ปัดเพื่อย้อนกลับ" "หากต้องการย้อนกลับไปที่หน้าจอล่าสุด ให้ปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ" - "หากต้องการย้อนกลับไปที่หน้าจอล่าสุด ให้ใช้ 2 นิ้วปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ" "ย้อนกลับ" "ปัดจากขอบด้านซ้ายหรือขวาไปตรงกลางหน้าจอ" "ปัดขึ้นจากขอบด้านล่างของหน้าจอ" "ไม่ต้องหยุดชั่วคราวก่อนยกนิ้วขึ้น" "ปัดขึ้นในแนวตรง" - "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว ต่อไปดูวิธีย้อนกลับ" - "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว" + "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าจอหลักสำเร็จแล้ว ต่อไปดูวิธีย้อนกลับ" + "คุณทำท่าทางสัมผัสเพื่อไปที่หน้าจอหลักสำเร็จแล้ว" "ปัดเพื่อไปที่หน้าแรก" "ปัดขึ้นจากด้านล่างของหน้าจอ ท่าทางสัมผัสนี้จะนำคุณไปที่หน้าจอหลักเสมอ" - "ใช้ 2 นิ้วปัดขึ้นจากด้านล่างของหน้าจอ ท่าทางสัมผัสนี้จะนำคุณไปที่หน้าจอหลักเสมอ" "ไปที่หน้าจอหลัก" "ปัดขึ้นจากด้านล่างของหน้าจอ" "เก่งมาก" "ปัดขึ้นจากขอบด้านล่างของหน้าจอ" "ลองแตะหน้าต่างค้างไว้นานขึ้นก่อนปล่อยนิ้ว" - "ตรวจสอบว่าปัดขึ้นในแนวตรง แล้วหยุดชั่วคราว" + "ปัดขึ้นในแนวตรง แล้วหยุดชั่วคราว" "คุณรู้วิธีใช้ท่าทางสัมผัสแล้ว หากต้องการปิดท่าทางสัมผัส ให้ไปที่การตั้งค่า" - "คุณทำท่าทางสัมผัสเพื่อสลับแอปเสร็จแล้ว" + "คุณทำท่าทางสัมผัสเพื่อเปลี่ยนแอปสำเร็จแล้ว" "ปัดเพื่อสลับแอป" "หากต้องการสลับระหว่างแอปต่างๆ ให้ปัดขึ้นจากด้านล่างของหน้าจอ ค้างไว้ แล้วปล่อย" - "หากต้องการสลับระหว่างแอป ให้ใช้ 2 นิ้วปัดขึ้นจากด้านล่างของหน้าจอค้างไว้แล้วปล่อย" "เปลี่ยนแอป" "ปัดขึ้นจากด้านล่างของหน้าจอ ค้างไว้ แล้วปล่อย" "เยี่ยมมาก" @@ -91,24 +92,33 @@ "ปัดขึ้นเพื่อไปที่หน้าแรก" "แตะปุ่มหน้าแรกเพื่อไปที่หน้าจอหลัก" "คุณเริ่มใช้%1$sได้แล้ว" - "อุปกรณ์" + "คุณเริ่มใช้อุปกรณ์ได้แล้ว" "การตั้งค่าการไปยังส่วนต่างๆ ของระบบ" + "%1$s ของคุณ\nพร้อมแล้ว" + "อุปกรณ์ของคุณ\nพร้อมแล้ว" + "สนุกกับ %1$s ใหม่ของคุณ" + "เลือกวิธีไปยังส่วนต่างๆ" + "ปัดขึ้น" + "แตะปุ่มหน้าแรก" + "แท็บเล็ต" + "โทรศัพท์" "แชร์" "ภาพหน้าจอ" "แยก" "บันทึกคู่แอป" "แตะแอปอื่นเพื่อใช้การแยกหน้าจอ" "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" - "ยกเลิก" + "ยกเลิก" "ออกจากการเลือกโหมดแยกหน้าจอ" "เลือกแอปอื่นเพื่อใช้การแยกหน้าจอ" "แอปหรือองค์กรของคุณไม่อนุญาตการดำเนินการนี้" "ยังไม่รองรับวิดเจ็ตในขณะนี้ โปรดเลือกแอปอื่น" - "ข้ามบทแนะนำการนำทางไหม" - "คุณดูบทแนะนำนี้ได้ภายหลังในแอป \"%1$s\"" - "ยกเลิก" - "ข้าม" "หมุนหน้าจอ" + "ภาพเคลื่อนไหวแสดงวิธีทำให้แถบงานปรากฏจากด้านล่างของหน้าจอและซ่อนโดยอัตโนมัติเมื่อไม่ได้ใช้งาน" + "ภาพเคลื่อนไหวแสดงวิธีปักหมุดแถบงานโดยใช้ปุ่มเปิด/ปิด เพื่อให้แถบงานยังคงปรากฏอยู่ด้านล่างของหน้าจอตลอดเวลา" + "ภาพเคลื่อนไหวแสดงวิธีสร้างโหมดแยกหน้าจอด้วยการลากและวางแอปจากแถบงานไว้เหนือแอปที่เปิดอยู่" + "ภาพเคลื่อนไหวแสดงวิธีเข้าถึงแอปที่แนะนำในอุปกรณ์" + "ภาพเคลื่อนไหวแสดงวิธีค้นหารายการบนหน้าจอโดยการแตะปุ่มดำเนินการค้างไว้และเลือกพื้นที่ที่รายการนั้นอยู่" "แถบงาน Education" "ลากแอปไปด้านข้างเพื่อใช้ 2 แอปพร้อมกัน" "ปัดขึ้นช้าๆ เพื่อแสดงแถบงาน" @@ -130,18 +140,40 @@ "การตั้งค่าด่วน" "แถบงาน" "แถบงานแสดงอยู่" - "แถบงานซ่อนอยู่" + "แถบงานและบับเบิลแสดงไว้ทางซ้าย" + "แถบงานและบับเบิลแสดงไว้ทางขวา" "แถบนำทาง" "แสดงแถบงานเสมอ" "เปลี่ยนโหมดการนําทาง" "ตัวแบ่งแถบงาน" + "แอปล่าสุดอื่นๆ" "ย้ายไปที่ด้านบนหรือด้านซ้าย" "ย้ายไปที่ด้านล่างหรือด้านขวา" - "{count,plural, =1{แสดงเพิ่มเติมอีก # แอป}other{แสดงเพิ่มเติมอีก # แอป}}" - "{count,plural, =1{แสดงแอปบนเดสก์ท็อป # รายการ}other{แสดงแอปบนเดสก์ท็อป # รายการ}}" + "เปิดแอปเป็นบับเบิล" + "แอปล่าสุด" + "รายการแอปล่าสุด" + "{count,plural, =1{แอปเพิ่มเติม}other{แอปเพิ่มเติม}}" + "เดสก์ท็อป" "%1$s และ %2$s" + "%1$s, รายการที่ %2$d จาก %3$d" + "เลื่อนไปทางซ้าย" + "เลื่อนไปทางขวา" "บับเบิล" "การดำเนินการเพิ่มเติม" "%1$s จาก %2$s" "%1$s และอีก %2$d รายการ" + "ย้ายไปทางซ้าย" + "ย้ายไปทางขวา" + "ปิดทั้งหมด" + "ขยาย %1$s" + "ยุบ %1$s" + "วงเพื่อค้นหา" + "ไอคอนแอป" + "ชื่อแอป" + "ปุ่มปิด" + "ปักหมุดไปยังแถบงาน" + "เลิกปักหมุดจากแถบงาน" + "กระตุ้นเตือน" + "ปิด" + "กระตุ้นเตือนเกี่ยวกับรูปภาพ" diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml index 978a5a37f8..bfe5d9a2c8 100644 --- a/quickstep/res/values-tl/strings.xml +++ b/quickstep/res/values-tl/strings.xml @@ -22,11 +22,14 @@ "I-pin" "Freeform" "Desktop" + "Ilipat sa external na display" + "I-clear" + "Desktop" "Walang kamakailang item" "Mga setting ng paggamit ng app" "I-clear lahat" + "Magdagdag ng bagong desk" "Mga kamakailang app" - "Isinara ang Gawain" "%1$s, %2$s" "< 1 min" "%1$s na lang ngayon" @@ -45,6 +48,7 @@ "Naka-enable ang mga iminumungkahing app" "Naka-disable ang mga iminumungkahing app" "Hinulaang app: %1$s" + "Tutorial sa Navigation gamit ang Galaw" "I-rotate ang iyong device" "Paki-rotate ang iyong device para tapusin ang tutorial sa navigation gamit ang galaw" "Tiyaking magsa-swipe ka mula sa dulong kanan o dulong kaliwang gilid" @@ -56,7 +60,6 @@ "Pumunta sa Settings para baguhin ang sensitivity ng pagbalik" "Mag-swipe para bumalik" "Para bumalik sa nakaraang screen, mag-swipe mula sa kaliwa o kanang gilid patungo sa gitna ng screen." - "Para bumalik sa huling screen, mag-swipe gamit ang 2 daliri mula sa kaliwa o kanang gilid hanggang sa gitna ng screen." "Bumalik" "Mag-swipe mula sa kaliwa o kanang gilid papunta sa gitna ng screen" "Tiyaking magsa-swipe ka pataas mula sa pinakaibaba ng screen" @@ -66,7 +69,6 @@ "Nakumpleto mo na ang galaw para pumunta sa home" "Mag-swipe para pumunta sa home" "Mag-swipe pataas mula sa ibaba ng iyong screen. Dadalhin ka palagi ng galaw na ito sa Home screen." - "Mag-swipe pataas gamit ang 2 daliri mula sa ibaba ng screen. Dadalhin ka palagi nito sa Home screen." "Pumunta sa home" "Mag-swipe pataas mula sa ibabang bahagi ng iyong screen" "Magaling!" @@ -77,7 +79,6 @@ "Nakumpleto mo na ang galaw para magpalipat-lipat sa mga app" "Mag-swipe para lumipat ng app" "Para lumipat ng app, mag-swipe pataas mula sa ibaba ng iyong screen, mag-hold, at iangat ang daliri." - "Para lumipat ng app, mag-swipe pataas gamit ang 2 daliri mula sa ibaba, mag-hold, at bumitaw." "Lumipat ng app" "Mag-swipe pataas mula sa ibaba ng iyong screen, i-hold ito saka bitawan" "Magaling!" @@ -91,24 +92,33 @@ "Mag-swipe pataas para pumunta sa home" "I-tap ang button ng home para pumunta sa iyong home screen" "Handa mo nang simulan ang paggamit sa iyong %1$s" - "device" + "Handa ka nang simulang gamitin ang iyong device" "Mga setting ng navigation ng system" + "Handa na \nang iyong %1$s!" + "Handa na \nang iyong device!" + "I-enjoy ang iyong bagong %1$s!" + "Piliin kung paano mag-navigate" + "Mag-swipe pataas" + "I-tap ang button ng home" + "tablet" + "telepono" "Ibahagi" "Screenshot" "Split" "I-save ang app pair" "Mag-tap ng ibang app para gamitin ang split screen" "Pumili ng ibang app para gamitin ang split screen" - "Kanselahin" + "Kanselahin" "Lumabas sa pagpili ng split screen" "Pumili ng ibang app para gamitin ang split screen" "Hindi pinapayagan ng app o ng iyong organisasyon ang pagkilos na ito" "Kasalukuyang hindi sinusuportahan ang mga widget, pumili ng ibang app" - "Laktawan ang tutorial sa pag-navigate?" - "Makikita mo ito sa %1$s app sa ibang pagkakataon" - "Kanselahin" - "Laktawan" "I-rotate ang screen" + "Animation na nagpapakita kung paano nakikita ang taskbar mula sa ibaba ng screen, at awtomatikong nakatago kapag hindi ginagamit" + "Animation na nagpapakita kung paano i-pin ang iyong taskbar gamit ang isang toggle, para mananatiling permanenteng nakikita sa ibaba ng screen ang taskbar" + "Animation na nagpapakita kung paano gumawa ng split screen, sa pamamagitan ng pag-drag at pag-drop ng app mula sa taskbar sa itaas ng nakabukas na app" + "Animation na nagpapakita kung paano i-access ang mga iminumungkahing app sa iyong device" + "Animation na nagpapakita kung paano maghanap ng isang item sa screen, sa pamamagitan ng pagpindot nang matagal sa action key at pagpili sa lugar kung saan naroroon ang item." "Impormasyon sa taskbar" "Mag-drag ng app sa gilid para makagamit ng 2 app nang sabay" "Mag-swipe nang mabagal pataas para ipakita ang Taskbar" @@ -130,18 +140,40 @@ "Quick Settings" "Taskbar" "Ipinapakita ang taskbar" - "Nakatago ang taskbar" + "Taskbar at bubble sa kaliwa" + "Taskbar at bubble sa kanan" "Navigation bar" "Ipakita lagi ang Taskbar" "Magpalit ng navigation mode" "Divider ng Taskbar" + "Iba pang kamakailang app" "Ilipat sa itaas/kaliwa" "Ilipat sa ibaba/kanan" - "{count,plural, =1{Magpakita ng # pang app.}one{Magpakita ng # pang app.}other{Magpakita ng # pang app.}}" - "{count,plural, =1{Ipakita ang # desktop app.}one{Ipakita ang # desktop app.}other{Ipakita ang # na desktop app.}}" + "Buksan ang app bilang bubble" + "Mga kamakailang app" + "Kamakailang listahan ng app" + "{count,plural, =1{pang app}one{pang app}other{pang app}}" + "Desktop" "%1$s at %2$s" + "%1$s, item %2$d ng %3$d" + "Mag-scroll pakaliwa" + "Mag-scroll pakanan" "Bubble" "Overflow" "%1$s mula sa %2$s" "%1$s at %2$d pa" + "Ilipat pakaliwa" + "Ilipat pakanan" + "I-dismiss lahat" + "i-expand ang %1$s" + "i-collapse ang %1$s" + "Circle to Search" + "Icon ng app" + "Pamagat ng app" + "Button na isara" + "I-pin sa taskbar" + "I-unpin sa taskbar" + "Pag-nudge" + "Isara" + "I-nudge ang larawan" diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml index 0cc5d7f856..e3d501d852 100644 --- a/quickstep/res/values-tr/strings.xml +++ b/quickstep/res/values-tr/strings.xml @@ -22,11 +22,14 @@ "Sabitle" "Serbest çalışma" "Masaüstü" + "Harici ekrana taşı" + "Temizle" + "Masaüstü" "Yeni öğe yok" "Uygulama kullanım ayarları" "Tümünü temizle" + "Yeni masa ekle" "Son uygulamalar" - "Görev Kapatıldı" "%1$s, %2$s" "< 1 dk." "Bugün %1$s kaldı" @@ -45,6 +48,7 @@ "Uygulama önerileri etkinleştirildi" "Uygulama önerileri devre dışı bırakıldı" "Tahmin edilen uygulama: %1$s" + "Hareketle Gezinme Eğitimi" "Cihazınızı döndürün" "Hareketle gezinme eğitimini tamamlamak için lütfen cihazınızı döndürün" "En sağ veya en sol kenardan kaydırdığınızdan emin olun" @@ -56,7 +60,6 @@ "Geri hareketinin hassasiyetini değiştirmek için Ayarlar\'a gidin" "Geri dönmek için kaydırma" "Son ekrana geri gitmek için sol veya sağ kenardan ekranın ortasına doğru kaydırın." - "Son ekrana geri gitmek için sol veya sağ kenardan ekranın ortasına doğru 2 parmağınızla kaydırın." "Geri dönme" "Sol veya sağ kenardan ekranın ortasına doğru kaydırın" "Ekranın alt kenarından yukarı kaydırdığınızdan emin olun" @@ -66,7 +69,6 @@ "Ana ekrana git hareketini tamamladınız" "Ana ekrana gitmek için kaydırma" "Ekranın alt kısmından yukarıya doğru kaydırın. Bu hareket sizi her zaman Ana ekrana götürür." - "Ekranın alt kısmından 2 parmağınızla yukarı kaydırın. Bu hareket sizi her zaman Ana ekrana götürür." "Ana sayfaya gitme" "Parmağınızı ekranın alt kısmından yukarıya doğru kaydırın" "Tebrikler!" @@ -77,7 +79,6 @@ "Uygulamalar arasında geçiş yapma hareketini tamamladınız" "Uygulamalar arasında geçiş yapmak için kaydırma" "Uygulamalar arasında geçiş yapmak için ekranınızın altından yukarı kaydırıp basılı tutun ve sonra bırakın." - "Uygulamalara geçiş yapmak için ekranın altından 2 parmakla yukarı kaydırıp basılı tutun ve bırakın." "Uygulamalar arasında geçiş yapma" "Ekranınızın alt tarafından yukarı doğru kaydırın, tutun ve sonra bırakın" "Tebrikler!" @@ -87,28 +88,37 @@ "Tekrar deneyin" "Güzel!" "Eğitim %1$d/%2$d" - "İşlem tamam!" + "Kurulum tamamlandı" "Ana ekrana gitmek için yukarı kaydırın" "Ana ekranınıza gitmek için ana sayfa düğmesine dokunun" - "%1$s cihazınızı kullanmaya hazırsınız" - "cihaz" + "Artık %1$s kullanılmak için hazır" + "Cihazınız kullanıma hazır" "Sistem gezinme ayarları" + "%1$s cihazınız \nhazır." + "Cihazınız \nhazır." + "Yeni %1$s cihazınızın tadını çıkarın." + "Nasıl gezineceğinizi seçin" + "Yukarı kaydırın" + "Ana sayfa düğmesine dokunun" + "tablet" + "telefon" "Paylaş" "Ekran görüntüsü" "Böl" "Uygulama çiftini kaydet" "Bölünmüş ekran için başka bir uygulamaya dokunun" "Bölünmüş ekran kullanmak için başka bir uygulama seçin" - "İptal" + "İptal" "Bölünmüş ekran seçiminden çıkın" "Bölünmüş ekran kullanmak için başka bir uygulama seçin" "Uygulamanız veya kuruluşunuz bu işleme izin vermiyor" "Widget\'lar şu anda desteklenmiyor. Lütfen başka bir uygulama seçin" - "Gezinme eğitimi atlansın mı?" - "Bunu daha sonra %1$s uygulamasında bulabilirsiniz" - "İptal" - "Atla" "Ekranı döndür" + "Görev çubuğunun ekranın altından nasıl göründüğünü ve kullanılmadığında otomatik olarak nasıl gizlendiğini gösteren animasyon" + "Görev çubuğunun, ekranın alt kısmında sürekli görünür olması için açma/kapatma düğmesini kullanarak görev çubuğunuzu nasıl sabitleyeceğinizi gösteren animasyon" + "Görev çubuğundaki bir uygulamayı, açık bir uygulamanın üzerine sürükleyip bırakarak nasıl bölünmüş ekran oluşturacağınızı gösteren animasyon" + "Cihazınızda önerilen uygulamalara nasıl erişeceğinizi gösteren animasyon" + "Eylem tuşuna dokunup basılı tutarak ve öğenin bulunduğu alanı seçerek ekranda nasıl öğe aranacağını gösteren animasyon" "Görev çubuğu eğitimi" "Aynı anda iki uygulama kullanmak için birini yana sürükleyin" "Görev çubuğunun görünmesi için yukarı doğru yavaşça kaydırın" @@ -130,18 +140,40 @@ "Hızlı Ayarlar" "Görev çubuğu." "Görev çubuğu gösteriliyor" - "Görev çubuğu gizlendi" + "Görev çubuğu ve baloncuklar solda gösteriliyor" + "Görev çubuğu ve baloncuklar sağda gösteriliyor" "Gezinme çubuğu" "Görev çubuğunu daima göster" "Gezinme modunu değiştir" "Görev Çubuğu Ayırıcısı" + "Diğer son uygulamalar" "Sol üste taşı" "Sağ alta taşı" - "{count,plural, =1{# uygulama daha göster.}other{# uygulama daha göster}}" - "{count,plural, =1{# masaüstü uygulamasını göster.}other{# masaüstü uygulamasını göster.}}" + "Uygulamayı balon olarak aç" + "Son uygulamalar" + "Son uygulama listesi" + "{count,plural, =1{uygulama daha}other{uygulama daha}}" + "Masaüstü" "%1$s ve %2$s" + "%1$s, %2$d/%3$d öğe" + "Sola kaydır" + "Sağa kaydır" "Balon" "Taşma" "%2$s uygulamasından %1$s" "%1$s ve %2$d tane daha" + "Sola taşı" + "Sağa taşı" + "Tümünü kapat" + "genişlet: %1$s" + "daralt: %1$s" + "Seçerek Arat" + "Uygulama simgesi" + "Uygulama başlığı" + "Kapat düğmesi" + "Çubuğa sabitle" + "Çubuktan kaldır" + "Otomatik hatırlatma" + "Kapat" + "Resmi otomatik hatırlat" diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml index 9c706a8403..06d5e29560 100644 --- a/quickstep/res/values-uk/strings.xml +++ b/quickstep/res/values-uk/strings.xml @@ -21,12 +21,15 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "Закріпити" "Довільна форма" - "Комп’ютер" + "Робочий стіл" + "Перемістити на зовнішній екран" + "Очистити" + "Комп’ютер" "Немає нещодавніх додатків" "Налаштування використання додатка" "Очистити все" + "Додати новий робочий стіл" "Нещодавні додатки" - "Завдання закрито" "%1$s, %2$s" "< 1 хв" "Сьогодні залишилося %1$s" @@ -45,6 +48,7 @@ "Рекомендовані додатки ввімкнено" "Рекомендовані додатки вимкнено" "Передбачений додаток: %1$s" + "Посібник із навігації за допомогою жестів" "Оберніть пристрій" "Обертайте пристрій, щоб ознайомитися з посібником із навігації за допомогою жестів" "Проведіть пальцем від самого краю екрана (правого або лівого)" @@ -56,7 +60,6 @@ "Щоб змінити чутливість жесту \"Назад\", відкрийте налаштування" "Щоб повернутися, проведіть пальцем по екрану" "Щоб перейти на попередній екран, проведіть пальцем від лівого чи правого краю до середини екрана." - "Щоб перейти на попередній екран, проведіть двома пальцями від лівого чи правого краю до середини екрана." "Повернення на попередній екран" "Проведіть пальцем від лівого чи правого краю до середини екрана" "Проведіть пальцем угору від нижнього краю екрана" @@ -66,7 +69,6 @@ "Ви виконали жест переходу на головний екран" "Проведіть пальцем, щоб перейти на головний екран" "Проведіть пальцем по екрану знизу вгору. Цей жест завжди повертатиме вас на головний екран." - "Проведіть двома пальцями вгору від низу екрана. Цей жест завжди спрямовує вас на головний екран." "Перехід на головний екран" "Проведіть пальцем угору від низу екрана" "Чудово!" @@ -77,7 +79,6 @@ "Ви виконали жест переходу в інший додаток" "Проведіть пальцем, щоб перейти в інший додаток" "Щоб переключатися між додатками, проведіть знизу вгору по екрану, утримуйте палець, а потім відпустіть." - "Щоб перейти в інший додаток, проведіть 2 пальцями від низу екрана, потримайте й відпустіть палець." "Перемикання між додатками" "Проведіть пальцем знизу вгору, утримуйте палець на екрані, а потім відпустіть" "Чудово!" @@ -91,24 +92,33 @@ "Щоб перейти на головний екран, проведіть пальцем угору" "Натисніть кнопку головного екрана, щоб відкрити його" "Тепер ви можете використовувати %1$s" - "пристрій" - "Системні налаштування навігації" + "Тепер ви можете користуватися пристроєм" + "Налаштування навігації в системі" + "Ваш %1$s \nготовий до використання!" + "Ваш пристрій \nготовий до використання!" + "Насолоджуйтеся своїм новим %1$sом!" + "Виберіть спосіб навігації" + "Проведіть пальцем угору" + "Натисніть кнопку головного екрана" + "планшет" + "телефон" "Поділитися" "Знімок екрана" "Розділити" "Зберегти пару" "Щоб розділити екран, виберіть ще один додаток." "Щоб розділити екран, виберіть ще один додаток." - "Скасувати" + "Скасувати" "Вийти з режиму розділення екрана" "Щоб розділити екран, виберіть ще один додаток." "Ця дія заборонена додатком або адміністратором організації" "Віджети наразі не підтримуються. Виберіть інший додаток." - "Пропустити посібник із навігації?" - "Ви знайдете його пізніше в додатку %1$s" - "Скасувати" - "Пропустити" "Обернути екран" + "Анімація, що показує, як панель завдань з’являється внизу екрана й автоматично приховується, якщо ви її не використовуєте" + "Анімація, що показує, як за допомогою перемикача закріпити панель завдань, щоб вона завжди відображалася внизу екрана" + "Анімація, що показує, як розділити екран, перетягнувши додаток із панелі завдань на відкритий додаток" + "Анімація, що показує, як отримати доступ до рекомендованих додатків на пристрої" + "Анімація, що показує, як шукати об’єкт на екрані, натиснувши й утримуючи клавішу дії, а потім вибравши область, де він розташований" "Панель завдань Education" "Перетягніть убік, щоб використовувати 2 додатки одночасно" "Щоб відкрити панель завдань, повільно проведіть угору" @@ -130,18 +140,40 @@ "Швидкі налаштув." "Панель завдань" "Панель завдань показано" - "Панель завдань приховано" + "Панель завдань і чати – зліва" + "Панель завдань і чати – справа" "Панель навігації" "Завжди показув. панель завдань" "Змінити режим навігації" "Розділювач панелі завдань" + "Інші нещодавні додатки" "Перемістити вгору або вліво" "Перемістити вниз або вправо" - "{count,plural, =1{Показати ще # додаток.}one{Показати ще # додаток.}few{Показати ще # додатки.}many{Показати ще # додатків.}other{Показати ще # додатка.}}" - "{count,plural, =1{Показати # комп’ютерну програму.}one{Показати # комп’ютерну програму.}few{Показати # комп’ютерні програми.}many{Показати # комп’ютерних програм.}other{Показати # комп’ютерної програми.}}" + "Відкрити додаток у спливаючому вікні" + "Нещодавні додатки" + "Список нещодавніх додатків" + "{count,plural, =1{інший додаток}one{інший додаток}few{інші додатки}many{інших додатків}other{іншого додатка}}" + "Комп’ютер" "%1$s та %2$s" + "%1$s, об’єкт %2$d з %3$d" + "Прокрутити вліво" + "Прокрутити вправо" "Повідомлення" "Додаткове повідомлення" "%1$s з додатка %2$s" "%1$s і ще %2$d" + "Перемістити вліво" + "Перемістити вправо" + "Закрити все" + "розгорнути \"%1$s\"" + "згорнути \"%1$s\"" + "Обвести й знайти" + "Значок додатка" + "Назва додатка" + "Кнопка \"Закрити\"" + "На панель завдань" + "З панелі завдань" + "Змістити" + "Закрити" + "Змістити зображення" diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml index e12524868f..d1ac26b303 100644 --- a/quickstep/res/values-ur/strings.xml +++ b/quickstep/res/values-ur/strings.xml @@ -22,11 +22,14 @@ "پن کریں" "فری فارم" "ڈیسک ٹاپ" + "بیرونی ڈسپلے پر متقل کریں" + "صاف کریں" + "ڈیسک ٹاپ" "کوئی حالیہ آئٹم نہیں" "ایپ کے استعمال کی ترتیبات" "سبھی کو صاف کریں" + "نیا ڈیسک شامل کریں" "حالیہ ایپس" - "ٹاسک بند ہے" "%1$s،%2$s" "‏< 1 منٹ" "آج %1$s بچا ہے" @@ -45,6 +48,7 @@ "ایپ کی تجاویز فعال ہیں" "ایپ کی تجاویز غیر فعال ہیں" "پیشن گوئی کردہ ایپ: %1$s" + "اشاروں والی نیویگیشن ٹیوٹوریل" "اپنا آلہ گھمائیں" "براہ کرم اشاروں والی نیویگیشن کا ٹیوٹوریل مکمل کرنے کے لیے اپنا آلہ گھمائیں" "یقینی بنائیں کہ آپ دائیں یا بائیں کنارے سے دور سے سوائپ کریں" @@ -56,7 +60,6 @@ "پچھلے اشارے کی حساسیت تبدیل کرنے کے لیے ترتیبات پر جائیں" "واپس جانے کے لیے سوائپ کریں" "پچھلی اسکرین پر واپس جانے کے لیے بائیں یا دائیں کنارے سے اسکرین کے وسط تک سوائپ کریں۔" - "آخری اسکرین پر واپس جانے کے لیے، 2 انگلیوں سے بائیں یا دائیں کنارے سے اسکرین کے وسط تک سوائپ کریں۔" "واپس جائیں" "دائیں یا بائیں کنارے سے اسکرین کے وسط تک سوائپ کریں" "اس بات کو یقینی بنائیں کہ آپ اسکرین کے نچلے کنارے سے اوپر کی طرف سوائپ کریں" @@ -66,7 +69,6 @@ "آپ نے ہوم پر جانے کا اشارہ مکمل کر لیا" "ہوم پر جانے کے لیے سوائپ کریں" "اپنی اسکرین کے نیچے سے اوپر کی طرف سوائپ کریں۔ یہ اشارہ آپ کو ہمیشہ ہوم اسکرین پر لے جاتا ہے۔" - "اسکرین کے نیچے سے 2 انگلیوں سے اوپر سوائپ کریں۔ یہ اشارہ آپ کو ہمیشہ ہوم اسکرین پر لے جاتا ہے۔" "ہوم پر جائیں" "اپنی اسکرین کے نچلے حصے سے اوپر کی طرف سوائپ کریں" "بہترین!" @@ -77,7 +79,6 @@ "آپ نے ایپس کو سوئچ کرنے کا اشارہ مکمل کر لیا" "ایپس سوئچ کرنے کے لیے سوائپ کریں" "ایپس کے مابین سوئچ کرنے کے لیے، اپنی اسکرین کے نچلے حصے سے اوپر کی جانب سوائپ کریں، پکڑے رکھیں، پھر چھوڑ دیں۔" - "ایپس کے مابین سوئچ کرنے کیلئے، اپنی اسکرین کے نیچے سے 2 انگلیوں سے اوپر سوائپ کریں، دبائے رکھیں پھر چھوڑ دیں۔" "ایپس سوئچ کریں" "اپنی اسکرین کے نچلے حصے سے اوپر کی جانب سوائپ کریں، دبائے رکھیں، پھر چھوڑ دیں" "بہت خوب!" @@ -91,24 +92,33 @@ "ہوم پر جانے کے لیے اوپر سوائپ کریں" "اپنی ہوم اسکرین پر جانے کے لیے ہوم بٹن پر تھپتھپائیں" "آپ اپنے %1$s کا استعمال شروع کرنے کے لیے تیار ہیں" - "آلہ" + "آپ اپنے آلے کا استعمال شروع کرنے کے لیے تیار ہیں" "سسٹم نیویگیشن کی ترتیبات" + "آپ کا %1$s \nتیار ہے!" + "آپ کا آلہ \nتیار ہے!" + "اپنے نئے %1$s کا لطف اٹھائیں!" + "‫""نیویگیٹ کرنے کا طریقہ منتخب کریں" + "اوپر کی طرف سوائپ کریں" + "ہوم بٹن پر تھپتھپائیں" + "ٹیبلیٹ" + "فون" "اشتراک کریں" "اسکرین شاٹ" "اسپلٹ" "ایپس کے جوڑے کو محفوظ کریں" "اسپلٹ اسکرین کا استعمال کرنے کیلئے دوسری ایپ پر تھپتھپائیں" "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" - "منسوخ کریں" + "منسوخ کریں" "اسپلٹ اسکرین کے انتخاب سے باہر نکلیں" "اسپلٹ اسکرین کے استعمال کیلئے دوسری ایپ منتخب کریں" "ایپ یا آپ کی تنظیم کی جانب سے اس کارروائی کی اجازت نہیں ہے" "وجیٹس فی الحال تعاون یافتہ نہیں ہیں، براہ کرم کوئی اور ایپ منتخب کریں" - "نیویگیشن کا ٹیوٹوریل نظر انداز کریں؟" - "آپ اسے بعد میں %1$s ایپ میں تلاش کر سکتے ہیں" - "منسوخ کریں" - "نظر انداز کریں" "اسکرین کو گھمائیں" + "اینیمیشن دکھاتی ہے کہ کس طرح ٹاسک بار اسکرین کے نیچے سے نظر آتا ہے اور استعمال میں نہ ہونے پر خود بخود چھپ جاتا ہے" + "اینیمیشن یہ دکھاتی ہے کہ ٹوگل کا استعمال کرتے ہوئے آپ کے ٹاسک بار کو کیسے پن کرنا ہے، تاکہ ٹاسک بار اسکرین کے نیچے مستقل طور پر نظر آئے" + "ایک کھلی ایپ کے اوپر ٹاسک بار سے کسی ایپ کو کھینچ کر ڈال کر، ایک اسپلٹ اسکرین بنانے کا طریقہ دکھانے والی اینیمیشن" + "اینیمیشن دکھاتی ہے کہ آپ کے آلے پر تجویز کردہ ایپس تک کیسے رسائی حاصل کی جائے" + "اینیمیشن دکھاتی ہے کہ ایکشن کلید کو ٹچ کر کے اور دبائے رکھ کر اور آئٹم جس ایریا میں ہے اسے منتخب کر کے اسکرین پر کسی آئٹم کو کس طرح تلاش کرنا ہے" "ٹاسک بار کی تعلیم" "ایک وقت میں 2 ایپس استعمال کرنے کیلئے ایپ سائیڈ پر گھسیٹیں" "ٹاسک بار دکھانے کے لیے آہستہ سے اوپر سوائپ کریں" @@ -130,18 +140,40 @@ "فوری ترتیبات" "ٹاسک بار" "ٹاشک بار دکھایا گیا" - "ٹاسک بار چھپایا گیا" + "ٹاسک بار و بلبلے بائیں طرف ہیں" + "ٹاسک بار و بلبلے دائیں طرف ہیں" "نیویگیشن بار" "ہمیشہ ٹاسک بار دکھائیں" "نیویگیشن موڈ تبدیل کریں" "ٹاسک بار ڈیوائیڈر" + "دیگر حالیہ ایپس" "اوپر/بائیں طرف منتقل کریں" "نیچے/دائیں طرف منتقل کریں" - "{count,plural, =1{# مزید ایپ دکھائیں۔}other{# مزید ایپس دکھائیں۔}}" - "{count,plural, =1{# ڈیسک ٹاپ ایپ دکھائیں۔}other{# ڈیسک ٹاپ ایپس دکھائیں۔}}" + "ایپ کو بطور ببل کھولیں" + "حالیہ ایپس" + "حالیہ ایپ کی فہرست" + "{count,plural, =1{مزید ایپ}other{مزید ایپس}}" + "ڈیسک ٹاپ" "%1$s اور %2$s" + "‫%1$s، آئٹم %2$d از %3$d" + "بائیں طرف اسکرول کریں" + "دائیں طرف اسکرول کریں" "ببل" "اوورفلو" "%2$s سے %1$s" "%1$s اور %2$d مزید" + "بائیں منتقل کریں" + "دائیں منتقل کریں" + "سبھی کو برخاست کریں" + "%1$s کو پھیلائیں" + "%1$s کو سکیڑیں" + "تلاش کرنے کیلئے دائرہ بنائیں" + "ایپ آئیکن" + "ایپ کا عنوان" + "\'بند کریں\' بٹن" + "ٹاسک بار میں پن کریں" + "ٹاسک بار سے پن ہٹائیں" + "کھسکائیں" + "بند کریں" + "تصویر کو کھسکائیں" diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml index 3f4f981095..dcd3ad614a 100644 --- a/quickstep/res/values-uz/strings.xml +++ b/quickstep/res/values-uz/strings.xml @@ -22,11 +22,14 @@ "Qadash" "Erkin shakl" "Desktop" + "Tashqi displeyga olish" + "Tozalash" + "Desktop" "Yaqinda ishlatilgan ilovalar yo‘q" "Ilovadan foydalanish sozlamalari" "Hammasini tozalash" + "Yangi ish stoli qoʻshish" "Oxirgi ilovalar" - "Vazifalar yopildi" "%1$s, %2$s" "< 1 daqiqa" "Bugun %1$s qoldi" @@ -45,6 +48,7 @@ "Ilova tavsiyalari yoqildi" "Endi ilova takliflari chiqmaydi" "Taklif etilgan ilova: %1$s" + "Ishorali navigatsiya darsligi" "Qurilmangizni buring" "Ishorali navigatsiya darsligini tugatish uchun qurilmani buring" "Ekran chetidan boshlab oʻngdan yoki chapdan suring" @@ -56,7 +60,6 @@ "Orqaga ishorasi sezuvchanligi Sozlamalardan oʻzgartiriladi" "Orqaga qaytish" "Ortga qaytish uchun barmoqni ekranning yon chekkalaridan oʻrtasigacha suring." - "Oxirgi ekranga qaytish uchun 2 barmoq bilan ekranning chap yoki oʻng chekkasidan oʻrtasigacha suring." "Orqaga" "Chap yoki oʻng chetidan ekranning oʻrtasiga suring" "Barmoqni ekranning pastki chetidan yuqoriga suring." @@ -66,7 +69,6 @@ "Bosh ekranni ochish ishorasi darsini tamomladingiz" "Svayp bilan bosh ekranni ochish" "Ekranning pastidan tepaga qarab suring. Bu ishora doim Bosh ekranni ochadi." - "2 barmoq bilan ekranning quyidan tepasiga suring. Bu ishora har doim Bosh ekranni ochadi." "Boshiga" "Ekranning quyi qismidan tepaga torting" "Barakalla!" @@ -77,7 +79,6 @@ "Ilovalarni almashtirish darsini tamomladingiz" "Ilovalar orasida almashish" "Ilovalarni ochish uchun ekranning pastidan tepaga qarab suring, biroz ushlab turing va qoʻyib yuboring" - "Ilovalarni almashtirish uchun 2 barmoq bilan ekranning quyidan tepasiga surib turib, qoʻyib yuboring" "Ilovalarni almashtirish" "Ekranning pastidan tepaga qarab suring, biroz ushlab turing va qoʻyib yuboring" "Juda soz!" @@ -90,25 +91,34 @@ "Hammasi tayyor!" "Boshiga qaytish uchun tepaga suring" "Bosh ekranga oʻtish uchun bosh ekran tugmasini bosing" - "%1$s xizmatga tayyor" - "qurilma" + "Sizning %1$s xizmatga tayyor" + "Qurilmangiz xizmatga tayyor" "Tizim navigatsiya sozlamalari" + "%1$s qurilmangiz \ntayyor!" + "Qurilmangiz \ntayyor!" + "Yangi %1$s qurilmangizdan bahra oling!" + "Qanday navigatsiya qilishni tanlang" + "Tepaga suring" + "Asosiy tugmani bosing" + "planshet" + "telefon" "Ulashish" "Skrinshot" "Ajratish" "Ilova juftini saqlash" "Ekranni ikkiga ajratish uchun boshqa ilovani bosing" "Ekranni ikkiga ajratish uchun boshqa ilovani tanlang" - "Bekor qilish" + "Bekor qilish" "Ekranni ikkiga ajratish tanlovidan chiqish" "Ekranni ikkiga ajratish uchun boshqa ilovani tanlang" "Bu amal ilova yoki tashkilotingiz tomonidan taqiqlangan" "Vidjetlar ishlamaydi. Boshqa ilovani tanlang" - "Navigatsiya darsi yopilsinmi?" - "Bu darslar %1$s ilovasida chiqadi" - "Bekor qilish" - "Tashlab ketish" "Ekranni burish" + "Vazifalar paneli ekranning pastki qismidan qanday koʻrinishini va ishlatilmayotganda avtomatik berkitilishi aks etgan animatsiya" + "Vazifalar panelini almashinuv tugmasi yordamida qanday qadashni koʻrsatuvchi animatsiya, shu bois vazifalar paneli ekranning pastki qismida doimiy chiqib turadi" + "Ochiq ilova tepasidagi vazifalar panelidan ilovani tortib tashlash orqali qanday ajratilgan ekran yaratish mumkinligi aks etgan animatsiya" + "Qurilmangizda tavsiya etilgan ilovalarga qanday kirishni aks ettiruvchi animatsiya" + "Amal tugmasini bosib turish va obyekt turgan sohani tanlash orqali ekranda obyektni qanday qidirish aks etgan animatsiya" "Vazifalar paneli qoʻllanmasi" "Bitta ilovani yon tomonga sudrab, bir vaqtda 2 ta ilovadan foydalaning." "Vazifalar panelini ochish uchun tepaga asta suring" @@ -130,18 +140,40 @@ "Tezkor sozlamalar" "Vazifalar paneli" "Vazifalar paneli ochiq" - "Vazifalar paneli yopiq" + "Panel va bulutchalar chapda" + "Panel va bulutchalar oʻngda" "Navigatsiya paneli" "Vazifalar paneli doim chiqarilsin" "Navigatsiya rejimini oʻzgartirish" "Vazifalar panelini ajratkich" + "Boshqa oxirgi ilovalar" "Yuqoriga yoki chapga oʻtkazish" "Pastga yoki oʻngga oʻtkazish" - "{count,plural, =1{Yana # ta ilovani chiqarish}other{Yana # ta ilovani chiqarish}}" - "{count,plural, =1{# ta desktop ilovani chiqarish.}other{# ta desktop ilovani chiqarish.}}" + "Ilovani qalqib chiquvchi oynada ochish" + "Oxirgi ilovalar" + "Oxirgi ilovalar roʻyxati" + "{count,plural, =1{boshqa ilova}other{boshqa ilovalar}}" + "Kompyuter" "%1$s va %2$s" + "%1$s, %2$d-element, jami: %3$d ta" + "Chapga varaqlash" + "Oʻngga varaqlash" "Pufak" "Kengaytirish" "%1$s (%2$s)" "%1$s va yana %2$d kishi" + "Chapga siljitish" + "Oʻngga siljitish" + "Hammasini yopish" + "%1$sni yoyish" + "%1$sni yigʻish" + "Chizib qidirish" + "Ilova belgisi" + "Ilova nomi" + "Yopish tugmasi" + "Panelga qadash" + "Vazifalar panelidan yechib olish" + "Eslatma" + "Yopish" + "Eslatma rasmi" diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml index 9bc526faac..6c96ea54f9 100644 --- a/quickstep/res/values-vi/strings.xml +++ b/quickstep/res/values-vi/strings.xml @@ -22,11 +22,14 @@ "Ghim" "Dạng tự do" "Máy tính" + "Chuyển sang màn hình ngoài" + "Xoá" + "Máy tính" "Không có mục gần đây nào" "Cài đặt mức sử dụng ứng dụng" "Xóa tất cả" + "Thêm không gian làm việc mới" "Ứng dụng gần đây" - "Đã đóng tác vụ" "%1$s, %2$s" "< 1 phút" "Hôm nay còn %1$s" @@ -45,20 +48,20 @@ "Đã bật tính năng Ứng dụng đề xuất" "Tính năng Ứng dụng đề xuất bị tắt" "Ứng dụng dự đoán: %1$s" + "Hướng dẫn thực hiện điều hướng bằng cử chỉ" "Xoay thiết bị của bạn" - "Vui lòng xoay thiết bị của bạn để hoàn tất hướng dẫn thao tác bằng cử chỉ" + "Vui lòng xoay thiết bị của bạn để hoàn tất hướng dẫn điều hướng bằng cử chỉ" "Hãy vuốt từ mép ngoài cùng bên phải hoặc ngoài cùng bên trái" "Hãy vuốt từ mép phải hoặc mép trái tới giữa màn hình rồi nhấc ngón tay ra" "Bạn đã học được cách vuốt từ mép phải để quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng." "Bạn đã thực hiện xong cử chỉ quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng." - "Bạn đã thực hiện xong cử chỉ quay lại" + "Bạn đã hoàn tất cử chỉ quay lại" "Hãy nhớ không được vuốt quá gần phần dưới cùng của màn hình" "Để thay đổi độ nhạy của cử chỉ quay lại, hãy vào mục Cài đặt" "Vuốt để quay lại" "Để quay lại màn hình gần đây nhất, hãy vuốt từ mép trái hoặc mép phải tới chính giữa màn hình." - "Để quay lại màn hình trước đó, hãy vuốt 2 ngón tay từ cạnh trái hoặc phải vào giữa màn hình." "Quay lại" - "Hãy vuốt từ mép trái hoặc mép phải tới giữa màn hình" + "Vuốt từ mép trái hoặc mép phải tới giữa màn hình" "Hãy vuốt lên từ mép dưới cùng của màn hình" "Hãy nhớ không được dừng trước khi nhấc ngón tay" "Hãy vuốt thẳng lên" @@ -66,7 +69,6 @@ "Bạn đã thực hiện xong cử chỉ chuyển đến Màn hình chính" "Vuốt để chuyển đến Màn hình chính" "Vuốt lên từ cuối màn hình. Cử chỉ này luôn đưa bạn đến Màn hình chính." - "Vuốt 2 ngón tay lên từ cuối màn hình. Cử chỉ này luôn đưa bạn về Màn hình chính." "Chuyển đến màn hình chính" "Vuốt lên từ mép dưới cùng của màn hình" "Tuyệt vời!" @@ -77,7 +79,6 @@ "Bạn đã thực hiện xong cử chỉ chuyển đổi ứng dụng" "Vuốt để chuyển đổi ứng dụng" "Để chuyển đổi ứng dụng, hãy vuốt lên từ cuối màn hình, giữ rồi thả ra." - "Để chuyển đổi giữa các ứng dụng, hãy vuốt 2 ngón tay lên từ cuối màn hình, giữ rồi thả ra." "Chuyển đổi ứng dụng" "Vuốt lên từ mép dưới cùng của màn hình, giữ rồi nhấc ngón tay ra" "Rất tốt!" @@ -91,30 +92,39 @@ "Vuốt lên để chuyển đến màn hình chính" "Nhấn vào nút màn hình chính để chuyển đến màn hình chính" "Bạn có thể bắt đầu sử dụng %1$s" - "thiết bị" - "Cài đặt cách thao tác trên hệ thống" + "Bạn có thể bắt đầu sử dụng thiết bị của mình" + "Cài đặt cách điều hướng trên hệ thống" + "%1$s của bạn đã \nsẵn sàng!" + "Thiết bị của bạn đã \nsẵn sàng!" + "Hãy trải nghiệm %1$s mới của bạn!" + "Chọn cách thao tác" + "Vuốt lên" + "Nhấn vào nút màn hình chính" + "máy tính bảng" + "điện thoại" "Chia sẻ" "Chụp ảnh màn hình" "Chia đôi màn hình" "Lưu cặp ứng dụng" "Nhấn vào ứng dụng khác để chia đôi màn hình" "Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình" - "Huỷ" + "Huỷ" "Thoát khỏi lựa chọn chia đôi màn hình" "Chọn một ứng dụng khác để dùng chế độ chia đôi màn hình" "Ứng dụng hoặc tổ chức của bạn không cho phép thực hiện hành động này" "Các tiện ích hiện không được hỗ trợ, vui lòng chọn một ứng dụng khác" - "Bỏ qua phần hướng dẫn thao tác?" - "Bạn có thể tìm lại phần hướng dẫn này trong ứng dụng %1$s" - "Hủy" - "Bỏ qua" "Xoay màn hình" + "Ảnh động minh hoạ cách thanh tác vụ xuất hiện từ cuối màn hình và tự động ẩn khi người dùng không sử dụng" + "Ảnh động minh hoạ cách ghim thanh tác vụ bằng nút bật/tắt để thanh tác vụ luôn hiển thị ở cuối màn hình" + "Ảnh động minh hoạ cách tạo chế độ chia đôi màn hình bằng cách kéo và thả một ứng dụng từ thanh tác vụ lên trên một ứng dụng đang mở" + "Ảnh động minh hoạ cách truy cập vào các ứng dụng được đề xuất trên thiết bị của bạn" + "Ảnh động minh hoạ cách tìm một mục trên màn hình bằng cách chạm và giữ phím hành động rồi chọn khu vực có mục đó" "Cách sử dụng thanh tác vụ" "Kéo một ứng dụng sang bên để dùng 2 ứng dụng cùng lúc" "Từ từ vuốt lên để Thanh tác vụ xuất hiện" "Nhận ứng dụng đề xuất dựa trên thói quen của bạn" "Nhấn và giữ trên đường phân chia để ghim Thanh tác vụ" - "Làm nhiều việc hơn qua Thanh tác vụ" + "Tăng năng suất qua Thanh tác vụ" "Luôn hiện Taskbar" "Để luôn hiện Taskbar ở cuối màn hình, hãy nhấn và giữ đường phân chia" "Chạm và giữ phím hành động để tìm nội dung trên màn hình của bạn" @@ -130,18 +140,40 @@ "Cài đặt nhanh" "Thanh tác vụ" "Đã hiện thanh thao tác" - "Đã ẩn thanh thao tác" + "Hiện thanh tác vụ, b.bóng trái" + "Hiện thanh tác vụ, b.bóng phải" "Thanh điều hướng" "Luôn hiện Thanh tác vụ" "Thay đổi chế độ điều hướng" "Đường phân chia Taskbar" + "Các ứng dụng khác gần đây" "Chuyển lên trên cùng/sang bên trái" "Chuyển xuống dưới cùng/sang bên phải" - "{count,plural, =1{Hiện thêm # ứng dụng.}other{Hiện thêm # ứng dụng.}}" - "{count,plural, =1{Hiện # ứng dụng dành cho máy tính.}other{Hiện # ứng dụng dành cho máy tính.}}" + "Mở ứng dụng dưới dạng bong bóng" + "Các ứng dụng gần đây" + "Danh sách ứng dụng gần đây" + "{count,plural, =1{ứng dụng khác}other{ứng dụng khác}}" + "Máy tính" "%1$s%2$s" + "%1$s, mục %2$d trong số %3$d" + "Cuộn sang trái" + "Cuộn sang phải" "Bong bóng" "Bong bóng bổ sung" "%1$s từ %2$s" "%1$s%2$d bong bóng khác" + "Di chuyển sang trái" + "Di chuyển sang phải" + "Đóng tất cả" + "mở rộng %1$s" + "thu gọn %1$s" + "Khoanh tròn để tìm kiếm" + "Biểu tượng ứng dụng" + "Tên ứng dụng" + "Nút đóng" + "Ghim vào thanh tác vụ" + "Bỏ ghim khỏi thanh tác vụ" + "Dịch chuyển" + "Đóng" + "Dịch chuyển hình ảnh" diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml index f6e446e803..d8f5247402 100644 --- a/quickstep/res/values-zh-rCN/strings.xml +++ b/quickstep/res/values-zh-rCN/strings.xml @@ -22,11 +22,14 @@ "固定" "自由窗口" "桌面" + "移至外接显示屏" + "清除" + "桌面设备" "近期没有任何内容" "应用使用设置" "全部清除" + "添加新桌面" "最近用过的应用" - "任务已关闭" "%1$s%2$s)" "不到 1 分钟" "今天还可使用 %1$s" @@ -45,28 +48,27 @@ "已启用应用建议" "已停用应用建议" "预测的应用:%1$s" + "手势导航教程" "请旋转设备" "请旋转设备,完成手势导航教程" "确保从最右侧或最左侧边缘开始滑动" "确保从右侧或左侧边缘滑动到屏幕中间位置后再松开手指" "您已了解如何使用“从右侧向左滑动”手势返回。接下来学习切换应用吧!" "您完成了“返回”手势教程。接下来了解如何切换应用。" - "您完成了“返回”手势" + "您已完成“返回”手势教程" "确保滑动时手的位置不要太靠近屏幕底部" "如要调节“返回”手势的灵敏度,请转到“设置”" "滑动即可返回" "如要返回上一个屏幕,请从屏幕左侧或右侧边缘往屏幕中间滑动。" - "若要返回上一个屏幕,请用两根手指从屏幕左侧或右侧边缘向中间滑动。" "返回" "从屏幕左侧或右侧边缘滑动到中间" "确保从屏幕底部边缘向上滑动" "松开手指前,请勿中途停顿" "确保笔直向上滑动" - "您完成了“转到主屏幕”手势。接下来了解如何返回。" - "您完成了“转到主屏幕”手势" + "您已完成“前往主屏幕”手势教程。接下来学习如何返回。" + "您已完成“前往主屏幕”手势教程" "上滑可转到主屏幕" "从屏幕底部向上滑动,即可随时回到主屏幕。" - "用两根手指从屏幕底部向上滑动,这个手势会一律使您回到主屏幕。" "前往主屏幕" "从屏幕底部向上滑动" "太棒了!" @@ -74,10 +76,9 @@ "尝试按住窗口较长时间,然后再松开手指" "确保笔直向上滑动,然后停住" "您已了解如何使用手势了。如要关闭手势,请前往“设置”。" - "您完成了应用切换手势" + "您已完成“切换应用”手势教程" "滑动即可切换应用" "如需在应用之间切换,请从屏幕底部向上滑动,按住,然后松开。" - "如需在应用之间切换,请从屏幕底部向上滑动,按住,然后松开。" "切换应用" "从屏幕底部向上滑动后按住,然后松开" "恭喜!" @@ -91,24 +92,33 @@ "向上滑动可前往主屏幕" "点按主屏幕按钮即可前往主屏幕" "您可以开始使用%1$s了" - "设备" + "您可以开始使用自己的设备了" "系统导航设置" + "您的 %1$s\n已设置完毕!" + "您的设备\n已设置完毕!" + "尽情享用新的%1$s吧!" + "选择导航方式" + "向上滑动" + "点按主屏幕按钮" + "平板电脑" + "手机" "分享" "屏幕截图" "分屏" - "保存应用组合" + "保存应用对" "点按另一个应用即可使用分屏" "另外选择一个应用才可使用分屏模式" - "取消" + "取消" "退出分屏选择模式" "另外选择一个应用才可使用分屏模式" "该应用或您所在的单位不允许执行此操作" "目前不支持微件,请选择其他应用" - "要跳过导航教程吗?" - "您之后可以在“%1$s”应用中找到此教程" - "取消" - "跳过" "旋转屏幕" + "该动画展示了任务栏如何显示在屏幕底部,以及如何在不使用时自动隐藏" + "该动画展示了如何使用切换开关固定任务栏,让任务栏始终显示在屏幕底部" + "该动画展示了如何通过将应用从任务栏拖放到打开的应用的上方,来创建分屏" + "该动画展示了如何在设备上访问推荐的应用" + "该动画展示了如何通过轻触并按住快捷操作按键并选择相应内容所在的区域,在界面中搜索相应内容" "任务栏教程" "将一个应用拖到一侧,即可同时使用两个应用" "缓慢上滑即可显示任务栏" @@ -130,18 +140,40 @@ "快捷设置" "任务栏" "任务栏已显示" - "任务栏已隐藏" + "已显示任务栏和左侧消息气泡" + "已显示任务栏和右侧消息气泡" "导航栏" "始终显示任务栏" "更改导航模式" "任务栏分隔线" + "最近用过的其他应用" "移到顶部/左侧" "移到底部/右侧" - "{count,plural, =1{显示另外 # 个应用。}other{显示另外 # 个应用。}}" - "{count,plural, =1{显示 # 款桌面应用。}other{显示 # 款桌面应用。}}" + "以气泡的形式打开应用" + "最近打开过的应用" + "“最近打开过的应用”列表" + "{count,plural, =1{多个应用}other{多个应用}}" + "桌面模式" "%1$s%2$s" + "%1$s,第 %2$d 项(共 %3$d 项)" + "向左滚动" + "向右滚动" "气泡框" "溢出式气泡框" "来自“%2$s”的%1$s" "%1$s以及另外 %2$d 个" + "左移" + "右移" + "全部关闭" + "展开“%1$s”" + "收起“%1$s”" + "圈定即搜" + "应用图标" + "应用名称" + "“关闭”按钮" + "固定到任务栏" + "从任务栏取消固定" + "智能推送" + "关闭" + "智能推送图片" diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml index b9d8eb765a..dae18618d6 100644 --- a/quickstep/res/values-zh-rHK/strings.xml +++ b/quickstep/res/values-zh-rHK/strings.xml @@ -22,11 +22,14 @@ "固定" "自由形式" "桌面" + "移至外部顯示屏" + "清除" + "桌面" "最近沒有任何項目" "應用程式使用情況設定" "全部清除" + "新增桌面" "最近使用的應用程式" - "閂咗工作" "%1$s%2$s" "少於 1 分鐘" "今天剩餘時間:%1$s" @@ -45,6 +48,7 @@ "已啟用應用程式建議" "已停用應用程式建議" "預測應用程式:%1$s" + "手勢導覽教學課程" "旋轉裝置方向" "請旋轉裝置方向以完成手勢導覽教學課程" "請確保從螢幕最右側或最左側邊緣滑動" @@ -56,7 +60,6 @@ "如要變更「返回」手勢的敏感度,請前往「設定」" "滑動即可返回" "如要返回上一個畫面,請從螢幕左側或右側邊緣往中央滑動。" - "如要返回上一個畫面,請用兩指從螢幕左側或右側邊緣往中央滑動。" "返回" "從螢幕左側或右側邊緣往中央滑動" "請確保從螢幕底部邊緣向上滑動" @@ -66,7 +69,6 @@ "你已完成「返回主畫面」手勢的教學課程" "向上滑動即可返回主畫面" "從螢幕底部向上滑動。這個手勢在所有畫面下都可讓你返回主畫面。" - "請用兩指從螢幕底部向上滑動。這個手勢在所有畫面下都可讓你返回主畫面。" "返回主畫面" "從螢幕底部向上滑動" "太好了!" @@ -77,7 +79,6 @@ "你已完成「切換應用程式」手勢的教學課程" "滑動即可切換應用程式" "如要切換應用程式,請從螢幕底部向上滑動並按住,然後放開。" - "如要切換應用程式,請用兩指從螢幕底部向上滑動並按住,然後放開手指。" "切換應用程式" "從螢幕底部向上滑動並按住,然後放開" "做得好!" @@ -91,24 +92,33 @@ "向上滑動即可前往主畫面" "輕按主按鈕即可前往主畫面" "你可以開始使用%1$s了" - "裝置" + "你現在可以開始使用裝置" "系統導覽設定" + "你的%1$s\n已準備就緒!" + "你的裝置\n已準備就緒!" + "盡情享用新%1$s!" + "選擇導覽方式" + "向上滑動" + "輕按主按鈕" + "平板電腦" + "手機" "分享" "螢幕截圖" "分割" "儲存應用程式組合" "輕按其他應用程式以使用分割螢幕" "選擇其他應用程式才能使用分割螢幕" - "取消" + "取消" "退出分割螢幕選取頁面" "選擇其他應用程式才能使用分割螢幕" "應用程式或你的機構不允許此操作" "目前不支援小工具,請選取其他應用程式" - "要略過手勢操作教學課程嗎?" - "你之後可以在「%1$s」應用程式找到這些說明" - "取消" - "略過" "旋轉螢幕" + "動畫顯示如何讓螢幕底部顯示工作列,並在閒置時自動隱藏" + "動畫顯示如何使用切換按鈕固定工作列,讓工作列在螢幕底部永久顯示" + "動畫顯示如何建立分割畫面,方法是從工作列拖曳應用程式,並放在已開啟應用程式的上方" + "動畫顯示如何在裝置上存取建議的應用程式" + "動畫顯示如何在畫面上搜尋項目,方法是按住快捷操作鍵,並選取項目所在區域" "工作列教學" "將應用程式拖曳到一邊,同時使用兩個應用程式" "慢慢向上掃即可顯示工作列" @@ -130,18 +140,40 @@ "快速設定" "工作列" "顯示咗工作列" - "隱藏咗工作列" + "工作列和對話氣泡在左邊顯示" + "工作列和對話氣泡在右邊顯示" "導覽列" "一律顯示工作列" "變更導覽模式" "工作列分隔線" + "其他最近使用的應用程式" "移至上方/左側" "移至底部/右側" - "{count,plural, =1{顯示另外 # 個應用程式。}other{顯示另外 # 個應用程式。}}" - "{count,plural, =1{顯示 # 個桌面應用程式。}other{顯示 # 個桌面應用程式。}}" + "在小視窗開啟應用程式" + "最近開啟的應用程式" + "「最近開啟的應用程式」清單" + "{count,plural, =1{個其他應用程式}other{個其他應用程式}}" + "桌面" "「%1$s」和「%2$s」" + "%1$s,第 %2$d 個項目,總共有 %3$d 項" + "向左捲動" + "向右捲動" "對話氣泡" "展開式" "%2$s 的「%1$s」通知" "%1$s和其他 %2$d 則通知" + "向左移" + "向右移" + "全部關閉" + "打開%1$s" + "收埋%1$s" + "一圈即搜" + "應用程式圖示" + "應用程式名稱" + "關閉按鈕" + "固定至工作列" + "取消固定至工作列" + "提醒" + "閂" + "提醒圖片" diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml index 90140cb587..4123edf30f 100644 --- a/quickstep/res/values-zh-rTW/strings.xml +++ b/quickstep/res/values-zh-rTW/strings.xml @@ -21,12 +21,15 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> "固定" "自由形式" - "電腦" + "電腦模式" + "移至外接螢幕" + "清除" + "電腦模式" "最近沒有任何項目" "應用程式使用情況設定" "全部清除" + "新增桌面" "最近使用的應用程式" - "工作已關閉" "%1$s (%2$s)" "< 1 分鐘" "今天還能使用 %1$s" @@ -45,6 +48,7 @@ "應用程式建議功能已啟用" "應用程式建議功能已停用" "預測的應用程式:%1$s" + "手勢操作教學課程" "旋轉裝置螢幕方向" "如要完成手勢操作教學課程,請旋轉裝置螢幕方向" "請務必從螢幕最右側或最左側滑動" @@ -56,7 +60,6 @@ "如要變更「返回」手勢的敏感度,請前往「設定」" "滑動即可返回" "如要返回上一個畫面,請從螢幕左側或右側邊緣往中央滑動。" - "如要返回上一個畫面,請用 2 指從螢幕左側或右側邊緣往中央滑動。" "返回" "從螢幕右側或左側往中央滑動" "請務必從螢幕底部向上滑動" @@ -66,7 +69,6 @@ "你已完成「返回主畫面」手勢的教學課程" "使用滑動手勢返回主畫面" "從螢幕底部向上滑動,即可返回主畫面。" - "用 2 指從螢幕底部向上滑動,即可回到主畫面。" "返回主畫面" "從螢幕底部向上滑動" "太棒了!" @@ -77,7 +79,6 @@ "你已完成「切換應用程式」手勢的教學課程" "使用滑動手勢切換應用程式" "如要切換不同的應用程式,請從螢幕底部向上滑動並按住,然後放開手指。" - "如要切換應用程式,請用 2 指從螢幕底部向上滑動並按住,然後放開手指。" "切換應用程式" "從螢幕底部向上滑動並按住,然後放開手指" "非常好!" @@ -91,28 +92,37 @@ "向上滑動即可前往主畫面" "輕觸主畫面按鈕即可前往主畫面" "你可以開始使用「%1$s」了" - "裝置" + "你可以開始使用裝置了" "系統操作機制設定" + "你的 %1$s\n已準備就緒!" + "你的裝置\n已準備就緒!" + "盡情享受全新%1$s!" + "選擇瀏覽方式" + "向上滑動" + "輕觸主畫面按鈕" + "平板電腦" + "手機" "分享" "螢幕截圖" "分割" "儲存應用程式配對" "輕觸另一個應用程式即可使用分割畫面" "選擇要在分割畫面中使用的另一個應用程式" - "取消" + "取消" "退出分割畫面選擇器" "必須選擇另一個應用程式才能使用分割畫面" "這個應用程式或貴機構不允許執行這個動作" "目前不支援小工具,請選取其他應用程式" - "要略過手勢操作教學課程嗎?" - "你之後可以在「%1$s」應用程式找到這些說明" - "取消" - "略過" "旋轉螢幕" + "這個動畫說明如何讓螢幕底部顯示工作列,並在閒置時自動隱藏" + "這個動畫說明如何使用切換鈕固定工作列,讓工作列永久顯示在螢幕底部" + "這個動畫說明如何建立分割畫面,方法是從工作列拖曳應用程式,並放在已開啟應用程式的上方" + "這個動畫說明如何在裝置上存取建議的應用程式" + "這個動畫說明如何在畫面上搜尋項目,方法是按住快捷操作鍵,並選取項目所在區域" "工作列教學課程" "將應用程式拖曳到一邊即可同時使用 2 個應用程式" "緩慢向上滑動讓工作列顯示在畫面上" - "根據你的日常安排建議應用程式" + "系統會根據你的日常安排,提供建議的應用程式。" "長按分隔線即可固定工作列" "充分發揮工作列的功用" "一律顯示工作列" @@ -130,18 +140,40 @@ "快速設定" "工作列" "已顯示工作列" - "已隱藏工作列" + "工作列和對話框顯示在左側" + "工作列和對話框顯示在右側" "導覽列" "一律顯示工作列" "變更操作模式" "工作列分隔線" + "最近曾使用的其他應用程式" "移到上方/左側" "移到底部/右側" - "{count,plural, =1{再多顯示 # 個應用程式。}other{再多顯示 # 個應用程式。}}" - "{count,plural, =1{顯示 # 個電腦版應用程式。}other{顯示 # 個電腦版應用程式。}}" + "以泡泡形式開啟應用程式" + "最近開啟的應用程式" + "「最近開啟的應用程式」清單" + "{count,plural, =1{個其他應用程式}other{個其他應用程式}}" + "電腦模式" "「%1$s」和「%2$s」" + "%1$s,第 %2$d 個項目,共 %3$d 項" + "向左捲動" + "向右捲動" "泡泡" "溢位" "「%2$s」的「%1$s」通知" "%1$s和另外 %2$d 則通知" + "向左移" + "向右移" + "全部關閉" + "展開「%1$s」" + "收合「%1$s」" + "畫圈搜尋" + "應用程式圖示" + "應用程式標題" + "關閉按鈕" + "固定到工作列" + "從工作列中取消固定" + "提醒" + "關閉" + "提醒圖片" diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml index 73be445a55..910a2b3430 100644 --- a/quickstep/res/values-zu/strings.xml +++ b/quickstep/res/values-zu/strings.xml @@ -22,11 +22,14 @@ "Phina" "I-Freeform" "Ideskithophu" + "Hambisa esibonisini sangaphandle" + "Sula" + "Ideskithophu" "Azikho izinto zakamuva" "Izilungiselelo zokusetshenziswa kohlelo lokusebenza" "Sula konke" + "Faka ideski elisha" "Izinhlelo zokusebenza zakamuva" - "Umsebenzi Uvaliwe" "%1$s, %2$s" "< 1 iminithi" "%1$s esele namhlanje" @@ -45,6 +48,7 @@ "Iziphakamiso zohlelo lokusebenza zinikwe amandla" "Iziphakamiso zohlelo lokusebenza zikhutshaziwe" "Uhlelo lokusebenza olubikezelwe: %1$s" + "Okokufundisa Kokuzulazula Kokuthinta" "Zungezisa idivayisi yakho" "Sicela uzungezise idivayisi yakho ukuze uqedele okokufundisa kokufuna ngokuthinta" "Qinisekisa ukuthi uswayipha ukusuka onqenqemeni olukude ngakwesokudla noma olukude ngakwesokunxele" @@ -56,7 +60,6 @@ "Ukuze ushintshe ukuzwela kokuthinta emuva, iya Kumasethingi" "Swayipha ukuze uye emuva" "Ukuze ubuyele emuva esikrinini sokugcina, swapha kusuka emngceleni wesobunxele noma wesokudla kuya phakathi kwesikrini." - "Ukuze ubuyele esikrinini sokugcina, swayipha ngeminwe emi-2 ukusuka kwesokunxele noma kwesokudla emphethweni uye phakathi kwesikrini." "Iya emuva" "Swayipha ukusuka kwesokunxele noma kwesokudla ukuya phakathi kwesikrini" "Qiniseka ukuthi uswayiphela phezulu ukusuka emngceleni ophansi wesikrini" @@ -66,7 +69,6 @@ "Ukuqedile ukuthinta kokuya ekhaya" "Swayipha ukuze uye ekhaya" "Swayiphela phezulu kusuka phansi kwesikrini sakho.Lokhu kuthinta kuhlala kukusa esikrinini sasekhaya." - "Swayiphela phezulu ngeminwe emi-2 kusukela phansi esikrinini. Lesi senzo sihlala sikuyisa esikrinini Sasekhaya." "Iya ekhasini lokuqala" "Swayiphela phezulu ukusuka phansi esikrinini sakho" "Umsebenzi omuhle!" @@ -77,7 +79,6 @@ "Ukuqedile ukuthinta kokushintsha ama-app" "Swayipha ukuze ushintshe ama-app" "Ukuze ushintshe phakathi kwama-app, swayiphela phezulu kusuka ngezansi kwesikrini sakho, bese uyadedela." - "Ukuze ushintshe phakathi kwama-app, swayiphela phezulu ngeminwe emi-2 kusukela phansi esikrinini sakho, ubambe, bese uyakhulula." "Shintsha ama-app" "Swayiphela phezulu ukusuka phansi esikrinini sakho, ubambe, bese uyadedela" "Wenze kahle!" @@ -91,24 +92,33 @@ "Swayiphela phezulu ukuze uye ekhaya" "Thepha inkinobho yasekhaya ukuze uye kusikrini sasekhaya" "Usulungele ukuqala ukusebenzisa i-%1$s yakho" - "idivayisi" + "Usulungele ukuqala ukusebenzisa idivayisi yakho" "Amasethingi wokuzulazula isistimu" + "I-%1$s yakho \nisilungile!" + "Idivayisi yakho \nisilungile!" + "Jabulela i-%1$s yakho entsha!" + "Khetha ukuthi uzofuna kanjani" + "Swayiphela phezulu" + "Thepha inkinobho yasekhaya" + "ithebhulethi" + "ifoni" "Yabelana" "Isithombe-skrini" "Hlukanisa" "Londoloza ukubhangqa i-app" "Thepha enye i-app ukuze usebenzise isikrini sokuhlukanisa" "Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini" - "Khansela" + "Khansela" "Phuma ekukhetheni ukuhlukaniswa kwesikrini" "Khetha enye i-app ukuze usebenzise ukuhlukanisa isikrini" "Lesi senzo asivunyelwanga uhlelo lokusebenza noma inhlangano yakho" "Amawijethi awasekelwe okwamanje, sicela ukhethe enye i-app" - "Yeqa isifundo sokuzulazula?" - "Lokhu ungakuthola kamuva ku-app ye-%1$s" - "Khansela" - "Yeqa" "Zungezisa isikrini" + "Upopayi obonisa ukuthi i-taskbar ivela kanjani phansi esikrinini, iphinde izifihle ngokuzenzakalelayo lapho ingasetshenziswa" + "Upopayi obonisa indlela yokuphina i-taskbar yakho usebenzisa ukuguqula, ukuze i-taskbar ihlale ibonakala unomphela phansi esikrinini." + "Upopayi obonisa ukuthi ungasakha kanjani isikrini esihlukanisayo, ngokuhudula nokudedela i-app ku-taskbar phezu kwe-app evuliwe." + "Upopayi obonisa indlela yokufinyelela ama-app aphakanyisiwe kudivayisi yakho" + "Upopayi obonisa indlela yokusesha into esikrinini, ngokuthinta uphinde ubambe inkinobho yokufinyelela nokukhetha indawo into ekuyo." "Imfundo ye-taskbar" "Hudula i-app ukusebenzisa ama-app ama-2 ngesikhathi esisodwa" "Swayiphela phezulu kancane ukuze ubonise i-Taskbar" @@ -130,18 +140,40 @@ "Amasethingi Asheshayo" "I-Taskbar" "Ibha yomsebenzi ibonisiwe" - "Ibha yomsebenzi ifihliwe" + "ITaskbar namabhamuza aboniswe kwesokunxele" + "ITaskbar namabhamuza aboniswe kwesokudla" "Ibha yokufuna" "Bonisa i-Taskbar njalo." "Shintsha imodi yokufuna" "Isihlukanisi se-Taskbar" + "Amanye ama-app akamuva" "Hamba phezulu/kwesokunxele" "Hamba phansi/kwesokudla" - "{count,plural, =1{Bonisa i-app e-# ngaphezulu.}one{Bonisa ama-app angu-# ngaphezulu.}other{Bonisa ama-app angu-# ngaphezulu.}}" - "{count,plural, =1{Bonisa i-app engu-# yedeskithophu.}one{Bonisa ama-app angu-# wedeskithophu.}other{Bonisa ama-app angu-# wedeskithophu.}}" + "Vula i-app njengebhamuza" + "Ama-app wakamuva" + "Uhlu lwe-app lwakamuva" + "{count,plural, =1{i-app eyengeziwe}one{ama-app engeziwe}other{ama-app engeziwe}}" + "Ideskithophu" "I-%1$s ne-%2$s" + "I-%1$s, into engu-%2$d kwezingu-%3$d" + "Skrolela ngakwesokunxele" + "Skrolela ngakwesokudla" "Ibhamuza" "Ukugcwala kakhulu" "%1$s kusuka ku-%2$s" "%1$s nokunye okungu-%2$d" + "Iya kwesokunxele" + "Iya kwesokudla" + "Chitha konke" + "nweba %1$s" + "goqa %1$s" + "Khethela Ukusesha" + "Isithonjana se-app" + "Isihloko se-app" + "Inkinobho yokuvala" + "Phina kutaskbar" + "Susa ukuphina i-taskbar" + "Isikhumbuzo" + "Vala" + "Gudluza isithombe" diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml index ccc7f180dd..46def35349 100644 --- a/quickstep/res/values/attrs.xml +++ b/quickstep/res/values/attrs.xml @@ -28,6 +28,16 @@ + + + + + + + @@ -35,6 +45,16 @@ + + + + + + + + + + #E0E0E0 @@ -31,7 +33,6 @@ #99000000 #EBffffff #99000000 - #646464 #ffffff @@ -76,7 +77,7 @@ #80868b #bdc1c6 - #FFFFFFFF + @android:color/system_neutral1_50 #333333 @@ -94,7 +95,10 @@ #f9ab00 - ?attr/colorAccentPrimary - ?attr/materialColorPrimaryFixedDim - @color/material_color_on_primary_fixed - + @color/materialColorPrimary + + #4285F4 + #7430E2 + #2A7EC8 + #2DB8BD + \ No newline at end of file diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index fd122103e1..f20d15c786 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -21,26 +21,11 @@ com.android.launcher3/com.android.quickstep.interaction.GestureSandboxActivity - com.android.quickstep.logging.StatsLogCompatManager com.android.quickstep.QuickstepTestInformationHandler - com.android.quickstep.util.SystemWindowManagerProxy - com.android.launcher3.uioverrides.QuickstepWidgetHolder$QuickstepHolderFactory - com.android.quickstep.InstantAppResolverImpl - com.android.launcher3.appprediction.PredictionAppTracker com.android.quickstep.QuickstepProcessInitializer - com.android.launcher3.model.QuickstepModelDelegate - com.android.launcher3.secondarydisplay.SecondaryDisplayPredictionsImpl - com.android.launcher3.taskbar.TaskbarModelCallbacksFactory - com.android.launcher3.taskbar.TaskbarViewCallbacksFactory - com.android.quickstep.LauncherRestoreEventLoggerImpl - com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl + com.android.launcher3.secondarydisplay.SecondaryDisplayQuickstepDelegateImpl com.android.launcher3.taskbar.TaskbarEduTooltipController - - - - com.android.launcher3.uioverrides.SystemApiWrapper - 3 @@ -54,10 +39,17 @@ 23 + 30dp + + + 6 + @@ -68,4 +60,12 @@ 44 + + + 0.65 + 850 + 0.8 + 2800 + 1 + 1600 diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 5c82c991c7..317e5b5e13 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -21,7 +21,7 @@ -1dp - 22dp + 26dp 4dp 2dp 216dp @@ -34,13 +34,12 @@ 0.7 - - 0.46 48dp 44dp + + 52dp 4dp @@ -67,6 +66,8 @@ 8dp 8dp + + 12dp 2dp @@ -82,6 +83,32 @@ 44dp 4dp + 28dp + + 20dp + + 6dp + 12dp + 8dp + 18dp + 20dp + 32dp + 16dp + + 24dp + 24dp + 72dp + 72dp + 72dp + 24dp + 24dp + + 28dp + + + 25dp + + 72dp 72dp 1.1 @@ -89,7 +116,10 @@ 24dp 48dp - 36dp + 16dp + 28dp + 10dp + 16dp 2.25dp @@ -101,6 +131,13 @@ 24dp 2dp + + 56dp + 2dp + + + 3dp + 600dp @@ -109,16 +146,18 @@ 0.0285dp 0.15dp 0.285dp + 0.7dp 1.4dp 36dp 0.5dp 115dp + 36dp 100dp - 16sp + 14sp 16dp 0dp @@ -179,6 +218,8 @@ 72dp 40dp 24dp + 142dp + 16dp 44dp @@ -263,9 +304,10 @@ 24dp - 40dp + 40dp 36sp 14sp + 16sp 24dp 32dp @@ -339,6 +381,9 @@ 108dp 316dp 4dp + 24dp + 108dp + 200% 10dp 1dp 112dp @@ -348,19 +393,19 @@ 48dp 48dp 32dp - 6dp 6dp + 0dp 64dp 64dp 48dp 1dp 72dp - 4dp - 14dp - 2dp - 2dp - 12dp - 2dp + 2dp + 12dp + 4dp + 6dp + 20dp + 4dp 12dp @@ -372,8 +417,10 @@ 32dp 8dp 400dp + 48dp 8dp 16dp + 4dp 16dp @@ -422,6 +469,10 @@ 300dp 16dp + 16dp + + + 8dp 60dp @@ -434,6 +485,7 @@ 55dp @dimen/transient_taskbar_stashed_height @dimen/taskbar_stashed_handle_height + @dimen/transient_taskbar_stash_spring_velocity_dp_per_s 9dp @@ -443,24 +495,29 @@ 80dp 1dp - 2dp + + 3dp 90dp + 20dp 32dp 36dp + 28dp 24dp 12dp 16dp 6dp 8dp + @dimen/bubblebar_icon_spacing 12dp 1dp + 12dp - 96dp + @dimen/floating_dismiss_background_size 60dp - 24dp - 50dp + @dimen/floating_dismiss_icon_size + 60dp 548dp 192dp 242dp @@ -473,31 +530,75 @@ 24dp 16dp + + 16dp + 4dp + 14dp + 238dp + 276dp + 12dp + 10dp + 1dp + 2dp + 20dp - 4dp + 5dp + 3dp + 2dp 104dp - 134dp + 136dp 52dp 20dp + 32dp 56dp + 24dp 16dp 16dp - 2dp + 4dp 28dp 16dp 24dp 8dp + 104dp + 360dp + 16dp + 20dp + 36dp + 56dp + 6dp + 16dp + 18dp 48dp - - - 220dp + + 14dp + 32dp + 24dp + + 428dp + 16dp + 8dp + 36dp + 8dp + 6dp + 12dp + 20dp + -10dp + + 16dp + + + 50 + 33 + 40dp + 0.8 + 380 diff --git a/quickstep/res/drawable/ic_bubble_dismiss_white.xml b/quickstep/res/values/ids.xml similarity index 51% rename from quickstep/res/drawable/ic_bubble_dismiss_white.xml rename to quickstep/res/values/ids.xml index b15111b821..4cc69ade5a 100644 --- a/quickstep/res/drawable/ic_bubble_dismiss_white.xml +++ b/quickstep/res/values/ids.xml @@ -1,6 +1,6 @@ - - - + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index f3066389d5..6e8bbb8de7 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -23,8 +23,15 @@ Pin Freeform - + Desktop + + Move to external display + + Clear + + + Desktop No recent items @@ -38,12 +45,12 @@ Back + + Add new desk + Recent apps - - Task Closed - %1$s, %2$s @@ -93,6 +100,9 @@ Predicted app: %1$s + + Gesture Navigation Tutorial + Rotate your device @@ -117,8 +127,6 @@ Swipe to go back To go back to the last screen, swipe from the left or right edge to the middle of the screen. - - To go back to the last screen, swipe with 2 fingers from the left or right edge to the middle of the screen. Go back @@ -137,8 +145,6 @@ Swipe to go home Swipe up from the bottom of your screen. This gesture always takes you to the Home screen. - - Swipe up with 2 fingers from the bottom of the screen. This gesture always takes you to the Home screen. Go home @@ -160,8 +166,6 @@ Swipe to switch apps To switch between apps, swipe up from the bottom of your screen, hold, then release. - - To switch between apps, swipe up with 2 fingers from the bottom of your screen, hold, then release. Switch apps @@ -215,12 +219,28 @@ Swipe up to go home Tap the home button to go to your home screen - + You\u2019re ready to start using your %1$s - - device + + You\u2019re ready to start using your device System navigation settings + + Your %1$s is \nready! + + Your device is \nready! + + Enjoy your new %1$s! + + Choose how to navigate + + Swipe up + + Tap the home button + + tablet + + phone @@ -234,7 +254,7 @@ Tap another app to use split screen Choose another app to use split screen - Cancel + Cancel Exit split screen selection Choose another app to use split screen @@ -242,20 +262,21 @@ This action isn\'t allowed by the app or your organization Widgets not currently supported, please select another app - - - Skip navigation tutorial? - - You can find this later in the %1$s app - - Cancel - - Skip Rotate screen + + Animation showing how the taskbar comes into view from the bottom of the screen, and automatically hides when not in use + + Animation showing how to pin your taskbar using a toggle, so the taskbar remains permanently visible at the bottom of the screen + + Animation showing how to create a split screen, by dragging and dropping an app from the taskbar above an open app + + Animation showing how to access suggested apps on your device + + Animation showing how to search for an item on screen, by touching and holding the action key and selecting the area the item is in Taskbar education @@ -296,10 +317,12 @@ Quick Settings Taskbar - + Taskbar shown - - Taskbar hidden + + Taskbar & bubbles left shown + + Taskbar & bubbles right shown Navigation bar @@ -308,27 +331,41 @@ Change navigation mode Taskbar Divider + + Other recent apps Move to top/left Move to bottom/right + + Open app as a bubble - + + Recent apps + + + Recent app list + + {count, plural, - =1{Show # more app.} - other{Show # more apps.} + =1{more app} + other{more apps} } - - {count, plural, - =1{Show # desktop app.} - other{Show # desktop apps.} - } + + Desktop %1$s and %2$s + + %1$s, item %2$d of %3$d + + + Scroll left + + Scroll right @@ -339,4 +376,38 @@ %1$s from %2$s %1$s and %2$d more + + Move left + + Move right + + Dismiss all + + expand %1$s + + collapse %1$s + + + Circle to Search + + + + App icon + + App title + + Close button + + + Pin to taskbar + + Unpin from taskbar + + + + Nudge + + Close + Nudge image diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml index 1da1166631..9f9c15ad8c 100644 --- a/quickstep/res/values/styles.xml +++ b/quickstep/res/values/styles.xml @@ -124,7 +124,7 @@ - + + @@ -268,53 +290,124 @@ + + + + - + + + + + + + + + + + + + + diff --git a/quickstep/res/xml/indexable_launcher_prefs.xml b/quickstep/res/xml/indexable_launcher_prefs.xml index b4740e5b51..cb31494b2e 100644 --- a/quickstep/res/xml/indexable_launcher_prefs.xml +++ b/quickstep/res/xml/indexable_launcher_prefs.xml @@ -29,4 +29,11 @@ android:defaultValue="false" android:persistent="true" /> + + diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java index f30cebc8f8..36b573f760 100644 --- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java +++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java @@ -27,7 +27,7 @@ import android.animation.AnimatorSet; import android.content.Context; import android.os.Build; import android.os.Handler; -import android.os.RemoteException; +import android.util.Log; import android.view.IRemoteAnimationFinishedCallback; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; @@ -223,6 +223,7 @@ public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat { if (skipFirstFrame) { // Because t=0 has the app icon in its original spot, we can skip the // first frame and have the same movement one frame earlier. + Log.d("b/311077782", "LauncherAnimationRunner.setAnimation"); mAnimator.setCurrentPlayTime( Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration())); } @@ -237,7 +238,7 @@ public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat { * animation finished runnable. */ @Override - public void onAnimationFinished() throws RemoteException { + public void onAnimationFinished() { mASyncFinishRunnable.run(); } } diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java index 523923df6a..4e4ffe7799 100644 --- a/quickstep/src/com/android/launcher3/LauncherInitListener.java +++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java @@ -15,11 +15,11 @@ */ package com.android.launcher3; -import com.android.quickstep.util.ActivityInitListener; +import com.android.quickstep.util.ContextInitListener; import java.util.function.BiPredicate; -public class LauncherInitListener extends ActivityInitListener { +public class LauncherInitListener extends ContextInitListener { /** * @param onInitListener a callback made when the activity is initialized. The callback should @@ -31,8 +31,8 @@ public class LauncherInitListener extends ActivityInitListener { } @Override - public boolean handleInit(Launcher launcher, boolean alreadyOnHome) { + public boolean handleInit(Launcher launcher, boolean isHomeStarted) { launcher.deferOverlayCallbacksUntilNextResumeOrStop(); - return super.handleInit(launcher, alreadyOnHome); + return super.handleInit(launcher, isHomeStarted); } } diff --git a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java index 962fd91c2e..08ef8fe219 100644 --- a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java +++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java @@ -15,10 +15,21 @@ */ package com.android.launcher3; +import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; + +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + import android.view.KeyEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.allapps.AllAppsRecyclerView; +import com.android.launcher3.allapps.SearchRecyclerView; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.uioverrides.PredictedAppIcon; import com.android.launcher3.uioverrides.QuickstepLauncher; @@ -26,13 +37,64 @@ import com.android.launcher3.uioverrides.QuickstepLauncher; import java.util.List; public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate { + private QuickstepLauncher mLauncher; public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) { super(launcher); + mLauncher = launcher; mActions.put(PIN_PREDICTION, new LauncherAction( PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P)); } + @Override + public void onPopulateAccessibilityEvent(View view, AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(view, event); + // Scroll to the position if focused view in main allapps list and not completely visible. + // Gate based on TYPE_VIEW_ACCESSIBILITY_FOCUSED for unintended scrolling with external + // mouse. + if (event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + scrollToPositionIfNeeded(view); + } + } + + private void scrollToPositionIfNeeded(View view) { + if (!Flags.accessibilityScrollOnAllapps()) { + return; + } + AllAppsRecyclerView contentView = mLauncher.getAppsView().getActiveRecyclerView(); + if (contentView instanceof SearchRecyclerView) { + return; + } + LinearLayoutManager layoutManager = (LinearLayoutManager) contentView.getLayoutManager(); + if (layoutManager == null) { + return; + } + RecyclerView.ViewHolder vh = contentView.findContainingViewHolder(view); + if (vh == null) { + return; + } + int itemPosition = vh.getBindingAdapterPosition(); + if (itemPosition == NO_POSITION) { + return; + } + int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition(); + int lastCompletelyVisible = layoutManager.findLastCompletelyVisibleItemPosition(); + boolean itemCompletelyVisible = firstCompletelyVisible <= itemPosition + && lastCompletelyVisible >= itemPosition; + if (itemCompletelyVisible) { + return; + } + RecyclerView.SmoothScroller smoothScroller = + new LinearSmoothScroller(mLauncher.asContext()) { + @Override + protected int getVerticalSnapPreference() { + return LinearSmoothScroller.SNAP_TO_ANY; + } + }; + smoothScroller.setTargetPosition(itemPosition); + layoutManager.startSmoothScroll(smoothScroller); + } + @Override protected void getSupportedActions(View host, ItemInfo item, List out) { if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) { diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 104e5e0e33..1a106a41e6 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -19,6 +19,7 @@ package com.android.launcher3; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.role.RoleManager.ROLE_HOME; import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; @@ -39,36 +40,36 @@ import static com.android.app.animation.Interpolators.DECELERATE_1_5; import static com.android.app.animation.Interpolators.DECELERATE_1_7; import static com.android.app.animation.Interpolators.EXAGGERATED_EASE; import static com.android.app.animation.Interpolators.LINEAR; +import static com.android.internal.util.LatencyTracker.ACTION_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE; +import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; import static com.android.launcher3.BaseActivity.INVISIBLE_ALL; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS; import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION; +import static com.android.launcher3.Flags.enableContainerReturnAnimations; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; +import static com.android.launcher3.Flags.syncAppLaunchWithTaskbarStash; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; -import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.Utilities.mapBoundToRange; -import static com.android.launcher3.config.FeatureFlags.ENABLE_BACK_SWIPE_HOME_ANIMATION; -import static com.android.launcher3.config.FeatureFlags.ENABLE_SCRIM_FOR_APP_LAUNCH; -import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION; import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY; import static com.android.launcher3.testing.shared.TestProtocol.WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE; import static com.android.launcher3.util.DisplayController.isTransientTaskbar; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; import static com.android.launcher3.views.FloatingIconView.getFloatingIconView; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch; import static com.android.quickstep.util.AnimUtils.clampToDuration; import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius; import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; +import static com.android.wm.shell.Flags.enableDynamicInsetsForAppLaunch; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -77,17 +78,15 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.ActivityOptions; import android.app.WindowConfiguration; +import android.app.role.RoleManager; import android.content.ComponentName; -import android.content.Context; import android.content.res.Resources; -import android.database.ContentObserver; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.IBinder; @@ -96,10 +95,9 @@ import android.os.Looper; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; -import android.provider.Settings.Global; +import android.util.Log; import android.util.Pair; import android.util.Size; -import android.view.CrossWindowBlurListeners; import android.view.IRemoteAnimationFinishedCallback; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; @@ -112,14 +110,19 @@ import android.view.WindowManager; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.window.DesktopModeFlags; import android.window.RemoteTransition; import android.window.TransitionFilter; +import android.window.WindowAnimationState; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; +import com.android.app.animation.Animations; +import com.android.app.animation.Interpolators; import com.android.internal.jank.Cuj; +import com.android.internal.util.LatencyTracker; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory; import com.android.launcher3.anim.AnimationSuccessListener; @@ -129,24 +132,24 @@ import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.shortcuts.DeepShortcutView; -import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.testing.shared.ResourceUtils; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.util.DynamicResource; -import com.android.launcher3.util.ObjectWrapper; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.StableViewInfo; import com.android.launcher3.util.Themes; import com.android.launcher3.views.FloatingIconView; -import com.android.launcher3.views.ScrimView; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.quickstep.LauncherBackAnimationController; import com.android.quickstep.RemoteAnimationTargets; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskViewUtils; +import com.android.quickstep.util.AlreadyStartedBackAnimState; +import com.android.quickstep.util.AnimatorBackState; +import com.android.quickstep.util.BackAnimState; import com.android.quickstep.util.MultiValueUpdateListener; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.RectFSpringAnim.DefaultSpringConfig; @@ -168,13 +171,17 @@ import com.android.systemui.animation.RemoteAnimationRunnerCompat; import com.android.systemui.shared.system.BlurUtils; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.QuickStepContract; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.startingsurface.IStartingWindowListener; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map.Entry; import app.lawnchair.compat.LawnchairQuickstepCompat; @@ -182,9 +189,10 @@ import app.lawnchair.compat.LawnchairQuickstepCompat; * Manages the opening and closing app transitions from Launcher */ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener { + private static final String TAG = "QuickstepTransitionManager"; private static final boolean ENABLE_SHELL_STARTING_SURFACE = - SystemProperties.getBoolean("persist.debug.shell_starting_surface", false); + SystemProperties.getBoolean("persist.debug.shell_starting_surface", true); /** Duration of status bar animations. */ public static final int STATUS_BAR_TRANSITION_DURATION = 120; @@ -217,17 +225,17 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public static final int CONTENT_ALPHA_DURATION = 217; public static final int TRANSIENT_TASKBAR_TRANSITION_DURATION = 417; + public static final int PINNED_TASKBAR_TRANSITION_DURATION = 600; public static final int TASKBAR_TO_APP_DURATION = 600; // TODO(b/236145847): Tune TASKBAR_TO_HOME_DURATION to 383 after conflict with unlock animation // is solved. private static final int TASKBAR_TO_HOME_DURATION_FAST = 300; private static final int TASKBAR_TO_HOME_DURATION_SLOW = 1000; public static final int CONTENT_SCALE_DURATION = 350; - public static final int CONTENT_SCRIM_DURATION = 350; private static final int MAX_NUM_TASKS = 5; - // Cross-fade duration between App Widget and App + // Cross-fade duration between App Widget and App when launching from widget. private static final int WIDGET_CROSSFADE_DURATION_MILLIS = 125; protected final QuickstepLauncher mLauncher; @@ -236,32 +244,29 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener protected final Handler mHandler; private final float mClosingWindowTransY; + private final float mClosingFreeformWindowTransY; private final float mMaxShadowRadius; private final StartingWindowListener mStartingWindowListener = new StartingWindowListener(this); - private ContentObserver mAnimationRemovalObserver = new ContentObserver( - ORDERED_BG_EXECUTOR.getHandler()) { - @Override - public void onChange(boolean selfChange) { - mAreAnimationsEnabled = Global.getFloat(mLauncher.getContentResolver(), - Global.ANIMATOR_DURATION_SCALE, 1f) > 0 - || Global.getFloat(mLauncher.getContentResolver(), - Global.TRANSITION_ANIMATION_SCALE, 1f) > 0; - } - }; + + // TODO(b/397690719): Investigate the memory leak from TaskStackChangeListeners#mImpl + // This is a temporary fix of memory leak b/397690719. We track registered + // {@link TaskRestartedDuringLaunchListener}, and remove them on activity destroy. + private final List mRegisteredTaskStackChangeListener = + new ArrayList<>(); private DeviceProfile mDeviceProfile; // Strong refs to runners which are cleared when the launcher activity is destroyed private RemoteAnimationFactory mWallpaperOpenRunner; private RemoteAnimationFactory mAppLaunchRunner; - private RemoteAnimationFactory mKeyguardGoingAwayRunner; private RemoteAnimationFactory mWallpaperOpenTransitionRunner; private RemoteTransition mLauncherOpenTransition; private final RemoteAnimationCoordinateTransfer mCoordinateTransfer; + private final LatencyTracker mLatencyTracker; private LauncherBackAnimationController mBackAnimationController; private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() { @@ -279,24 +284,27 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Pairs of window starting type and starting window background color for starting tasks // Will never be larger than MAX_NUM_TASKS private LinkedHashMap> mTaskStartParams; - private boolean mAreAnimationsEnabled = true; private final Interpolator mOpeningXInterpolator; private final Interpolator mOpeningInterpolator; - public QuickstepTransitionManager(Context context) { - mLauncher = Launcher.cast(Launcher.getLauncher(context)); + private final SystemUiProxy mSystemUiProxy; + + public QuickstepTransitionManager(QuickstepLauncher launcher) { + mLauncher = launcher; mDragLayer = mLauncher.getDragLayer(); mHandler = new Handler(Looper.getMainLooper()); mDeviceProfile = mLauncher.getDeviceProfile(); mBackAnimationController = new LauncherBackAnimationController(mLauncher, this); - checkAndMonitorIfAnimationsAreEnabled(); Resources res = mLauncher.getResources(); mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y); + mClosingFreeformWindowTransY = + res.getDimensionPixelSize(R.dimen.closing_freeform_window_trans_y); mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius); mLauncher.addOnDeviceProfileChangeListener(this); + mSystemUiProxy = SystemUiProxy.INSTANCE.get(mLauncher); if (ENABLE_SHELL_STARTING_SURFACE) { mTaskStartParams = new LinkedHashMap<>(MAX_NUM_TASKS) { @@ -306,14 +314,15 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } }; - SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener( - mStartingWindowListener); + mSystemUiProxy.setStartingWindowListener(mStartingWindowListener); } - mOpeningXInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.app_open_x); - mOpeningInterpolator = AnimationUtils.loadInterpolator(context, - R.interpolator.emphasized_interpolator); + mOpeningXInterpolator = AnimationUtils.loadInterpolator( + launcher, R.interpolator.app_open_x); + mOpeningInterpolator = AnimationUtils.loadInterpolator( + launcher, R.interpolator.emphasized_interpolator); mCoordinateTransfer = new RemoteAnimationCoordinateTransfer(mLauncher); + mLatencyTracker = LatencyTracker.getInstance(launcher); } @Override @@ -326,7 +335,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @return ActivityOptions with remote animations that controls how the window of the opening * targets are displayed. */ - public ActivityOptionsWrapper getActivityLaunchOptions(View v) { + public ActivityOptionsWrapper getActivityLaunchOptions(View v, ItemInfo itemInfo) { boolean fromRecents = isLaunchingFromRecents(v, null /* targets */); RunnableList onEndCallback = new RunnableList(); @@ -334,19 +343,16 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener TaskRestartedDuringLaunchListener restartedListener = new TaskRestartedDuringLaunchListener(); restartedListener.register(onEndCallback::executeAllAndDestroy); - onEndCallback.add(restartedListener::unregister); - - mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback); - ItemInfo tag = (ItemInfo) v.getTag(); - if (tag != null && tag.shouldUseBackgroundAnimation()) { - ContainerAnimationRunner containerAnimationRunner = ContainerAnimationRunner.from( - v, mLauncher, mStartingWindowListener, onEndCallback); - if (containerAnimationRunner != null) { - mAppLaunchRunner = containerAnimationRunner; + mRegisteredTaskStackChangeListener.add(restartedListener); + onEndCallback.add(new Runnable() { + @Override + public void run() { + restartedListener.unregister(); + mRegisteredTaskStackChangeListener.remove(restartedListener); } - } - RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner( - mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */); + }); + + RemoteAnimationRunnerCompat runner = createAppLaunchRunner(v, onEndCallback); // Note that this duration is a guess as we do not know if the animation will be a // recents launch or not for sure until we know the opening app targets. @@ -361,12 +367,48 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener LawnchairQuickstepCompat.getRemoteTransitionCompat().getRemoteTransition(runner.toRemoteTransition(), mLauncher.getIApplicationThread(), "QuickstepLaunch"), "Lawnchair"); - IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback); + IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback, mLauncher); options.setOnAnimationAbortListener(endCallback); options.setOnAnimationFinishedListener(endCallback); + options.setLaunchCookie(StableViewInfo.toLaunchCookie(itemInfo)); + + // Prepare taskbar for animation synchronization. This needs to happen here before any + // app transition is created. + LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController(); + if (syncAppLaunchWithTaskbarStash() + && enableScalingRevealHomeAnimation() + && mLauncher.getStateManager().getState() == NORMAL + && taskbarController != null) { + taskbarController.setIgnoreInAppFlagForSync(true); + mLauncher.addEventCallback(EVENT_DESTROYED, onEndCallback::executeAllAndDestroy); + onEndCallback.add(() -> { + taskbarController.setIgnoreInAppFlagForSync(false); + }); + } + return new ActivityOptionsWrapper(options, onEndCallback); } + /** + * Selects the appropriate type of launch runner for the given view, builds it, and returns it. + * {@link QuickstepTransitionManager#mAppLaunchRunner} is updated as a by-product of this + * method. + */ + private RemoteAnimationRunnerCompat createAppLaunchRunner(View v, RunnableList onEndCallback) { + ItemInfo tag = (ItemInfo) v.getTag(); + ContainerAnimationRunner containerRunner = null; + if (tag != null && tag.shouldUseBackgroundAnimation()) { + containerRunner = ContainerAnimationRunner.fromView( + v, true /* forLaunch */, mLauncher, mStartingWindowListener, onEndCallback, + null /* windowState */); + } + + mAppLaunchRunner = containerRunner != null + ? containerRunner : new AppLaunchAnimationRunner(v, onEndCallback); + return new LauncherAnimationRunner( + mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */); + } + /** * Whether the launch is a recents app transition and we should do a launch animation * from the recents view. Note that if the remote animation targets are not provided, this @@ -378,7 +420,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @return true if the app is launching from recents, false if it most likely is not */ protected boolean isLaunchingFromRecents(@NonNull View v, - @Nullable RemoteAnimationTarget[] targets) { + @Nullable RemoteAnimationTarget[] targets) { return mLauncher.getStateManager().getState().isRecentsViewVisible && findTaskViewToLaunch(mLauncher.getOverviewPanel(), v, targets) != null; } @@ -392,12 +434,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @param launcherClosing true if the launcher app is closing */ protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v, - @NonNull RemoteAnimationTarget[] appTargets, - @NonNull RemoteAnimationTarget[] wallpaperTargets, - @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) { + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) { TaskViewUtils.composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing, mLauncher.getStateManager(), - mLauncher.getOverviewPanel(), mLauncher.getDepthController()); + mLauncher.getOverviewPanel(), mLauncher.getDepthController(), + /* transitionInfo= */ null); } private boolean areAllTargetsTranslucent(@NonNull RemoteAnimationTarget[] targets) { @@ -421,10 +464,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @param launcherClosing true if launcher is closing */ private void composeIconLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v, - @NonNull RemoteAnimationTarget[] appTargets, - @NonNull RemoteAnimationTarget[] wallpaperTargets, - @NonNull RemoteAnimationTarget[] nonAppTargets, - boolean launcherClosing) { + @NonNull RemoteAnimationTarget[] appTargets, + @NonNull RemoteAnimationTarget[] wallpaperTargets, + @NonNull RemoteAnimationTarget[] nonAppTargets, + boolean launcherClosing) { // Set the state animation first so that any state listeners are called // before our internal listeners. mLauncher.getStateManager().setCurrentAnimation(anim); @@ -467,14 +510,16 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * figure out where the floating view should animate to. */ private Rect getWindowTargetBounds(@NonNull RemoteAnimationTarget[] appTargets, - int rotationChange) { + int rotationChange) { RemoteAnimationTarget target = null; for (RemoteAnimationTarget t : appTargets) { if (t.mode != MODE_OPENING) continue; target = t; break; } - if (target == null) return new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); + final int widthPx = mDeviceProfile.getDeviceProperties().getWidthPx(); + final int heightPx = mDeviceProfile.getDeviceProperties().getHeightPx(); + if (target == null) return new Rect(0, 0, widthPx, heightPx); final Rect bounds = new Rect(target.screenSpaceBounds); if (target.localBounds != null) { bounds.set(target.localBounds); @@ -484,19 +529,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener if (rotationChange != 0) { if ((rotationChange % 2) == 1) { // undoing rotation, so our "original" parent size is actually flipped - Utilities.rotateBounds(bounds, mDeviceProfile.heightPx, mDeviceProfile.widthPx, + Utilities.rotateBounds(bounds, heightPx, widthPx, 4 - rotationChange); } else { - Utilities.rotateBounds(bounds, mDeviceProfile.widthPx, mDeviceProfile.heightPx, + Utilities.rotateBounds(bounds, widthPx, heightPx, 4 - rotationChange); } } - if (mDeviceProfile.isTaskbarPresentInApps - && !target.willShowImeOnTarget - && !isTransientTaskbar(mLauncher)) { - // Animate to above the taskbar. - bounds.bottom -= target.contentInsets.bottom; - } return bounds; } @@ -512,7 +551,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @param skipAllAppsScale True if we want to avoid scaling All Apps */ private Pair getLauncherContentAnimator(boolean isAppOpening, - int startDelay, boolean skipAllAppsScale) { + int startDelay, boolean skipAllAppsScale) { AnimatorSet launcherAnimator = new AnimatorSet(); Runnable endListener; @@ -532,7 +571,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener final View appsView = mLauncher.getAppsView(); final float startAlpha = appsView.getAlpha(); final float startScale = SCALE_PROPERTY.get(appsView); - if (mDeviceProfile.isTablet) { + if (mDeviceProfile.getDeviceProperties().isTablet()) { + // AllApps should not fade at all in tablets. alphas = new float[]{1, 1}; } @@ -569,57 +609,50 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener endListener = composeViewContentAnimator(launcherAnimator, alphas, scales); } else { List viewsToAnimate = new ArrayList<>(); - Workspace workspace = mLauncher.getWorkspace(); - workspace.forEachVisiblePage( - view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets())); + viewsToAnimate.add(mLauncher.getWorkspace()); + Hotseat hotseat = mLauncher.getHotseat(); // Do not scale hotseat as a whole when taskbar is present, and scale QSB only if it's // not inline. if (mDeviceProfile.isTaskbarPresent) { if (!mDeviceProfile.isQsbInline) { - viewsToAnimate.add(mLauncher.getHotseat().getQsb()); + viewsToAnimate.add(hotseat.getQsb()); } } else { - viewsToAnimate.add(mLauncher.getHotseat()); + viewsToAnimate.add(hotseat); } viewsToAnimate.forEach(view -> { view.setLayerType(View.LAYER_TYPE_HARDWARE, null); - ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scales) + // Start the animation from the current value, instead of assuming the views are + // in their resting state, so interrupted animations merge seamlessly. + // TODO(b/367591368): ideally these animations would be refactored to be + // controlled centrally so each instances doesn't need to care about this + // coordination. + float[] scale = new float[]{view.getScaleX(), scales[1]}; + + // Cancel any ongoing animations. This is necessary to avoid a conflict between + // e.g. the unfinished animation triggered when closing an app back to Home and + // this animation caused by a launch. + Animations.Companion.cancelOngoingAnimation(view); + // Make sure to cache the current animation, so it can be properly interrupted. + Animations.Companion.setOngoingAnimation(view, launcherAnimator); + + ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scale) .setDuration(CONTENT_SCALE_DURATION); scaleAnim.setInterpolator(DECELERATE_1_5); launcherAnimator.play(scaleAnim); }); - final boolean scrimEnabled = ENABLE_SCRIM_FOR_APP_LAUNCH.get(); - if (scrimEnabled) { - int scrimColor = Themes.getAttrColor(mLauncher, R.attr.overviewScrimColor); - int scrimColorTrans = ColorUtils.setAlphaComponent(scrimColor, 0); - int[] colors = isAppOpening - ? new int[]{scrimColorTrans, scrimColor} - : new int[]{scrimColor, scrimColorTrans}; - ScrimView scrimView = mLauncher.getScrimView(); - if (scrimView.getBackground() instanceof ColorDrawable) { - scrimView.setBackgroundColor(colors[0]); - - ObjectAnimator scrim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR, - colors); - scrim.setDuration(CONTENT_SCRIM_DURATION); - scrim.setInterpolator(DECELERATE_1_5); - - launcherAnimator.play(scrim); - } - } - endListener = () -> { viewsToAnimate.forEach(view -> { SCALE_PROPERTY.set(view, 1f); view.setLayerType(View.LAYER_TYPE_NONE, null); + + // Reset the cached animation. + Animations.Companion.setOngoingAnimation(view, null /* animation */); }); - if (scrimEnabled) { - mLauncher.getScrimView().setBackgroundColor(Color.TRANSPARENT); - } mLauncher.resumeExpensiveViewUpdates(); }; } @@ -637,7 +670,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @return listener to run when the animation ends */ protected Runnable composeViewContentAnimator(@NonNull AnimatorSet anim, - float[] alphas, float[] scales) { + float[] alphas, float[] scales) { RecentsView overview = mLauncher.getOverviewPanel(); ObjectAnimator alpha = ObjectAnimator.ofFloat(overview, RecentsView.CONTENT_ALPHA, alphas); @@ -659,16 +692,35 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener }; } + private boolean shouldCropToInset(RemoteAnimationTarget target) { + return enableDynamicInsetsForAppLaunch() + && mDeviceProfile.isTaskbarPresent + && mDeviceProfile.isTaskbarPresentInApps + && target != null && !target.willShowImeOnTarget + && !isTransientTaskbar(mLauncher); + } + /** * @return Animator that controls the window of the opening targets from app icons. */ private Animator getOpeningWindowAnimators(View v, - RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets, - RemoteAnimationTarget[] nonAppTargets, - boolean launcherClosing) { + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, + boolean launcherClosing) { + RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets, + wallpaperTargets, nonAppTargets, MODE_OPENING); int rotationChange = getRotationChange(appTargets); Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange); + final int[] bottomInsetPos = new int[]{ + mSystemUiProxy.getHomeVisibilityState().getNavbarInsetPosition()}; + final RemoteAnimationTarget target = openingTargets.getFirstAppTarget(); + final boolean cropToInset = shouldCropToInset(target); + if (cropToInset) { + // Animate to above the taskbar. + windowTargetBounds.bottom = Math.min(bottomInsetPos[0], + windowTargetBounds.bottom); + } boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets); RectF launcherIconBounds = new RectF(); @@ -681,8 +733,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener Rect crop = new Rect(); Matrix matrix = new Matrix(); - RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets, - wallpaperTargets, nonAppTargets, MODE_OPENING); SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(floatingView); openingTargets.addReleaseCheck(surfaceApplier); @@ -751,7 +801,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources()) ? Math.max(crop.width(), crop.height()) / 2f : 0f; - final float finalWindowRadius = mDeviceProfile.isMultiWindowMode + final float finalWindowRadius = mDeviceProfile.getDeviceProperties().isMultiWindowMode() ? 0 : getWindowCornerRadius(mLauncher); final float finalShadowRadius = appTargetsAreTranslucent ? 0 : mMaxShadowRadius; @@ -788,6 +838,39 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener @Override public void onUpdate(float percent, boolean initOnly) { + if (cropToInset && bottomInsetPos[0] != mSystemUiProxy.getHomeVisibilityState() + .getNavbarInsetPosition()) { + final RemoteAnimationTarget target = openingTargets.getFirstAppTarget(); + bottomInsetPos[0] = mSystemUiProxy.getHomeVisibilityState() + .getNavbarInsetPosition(); + final Rect bounds = target != null + ? target.screenSpaceBounds : windowTargetBounds; + // Animate to above the taskbar. + int bottomLevel = Math.min(bottomInsetPos[0], bounds.bottom); + windowTargetBounds.bottom = bottomLevel; + final int endHeight = bottomLevel - bounds.top; + + AnimOpenProperties prop = new AnimOpenProperties(mLauncher.getResources(), + mDeviceProfile, windowTargetBounds, launcherIconBounds, v, + dragLayerBounds[0], dragLayerBounds[1], hasSplashScreen, + floatingView.isDifferentFromAppIcon()); + mCropRectCenterY = new FloatProp(prop.cropCenterYStart, prop.cropCenterYEnd, + mOpeningInterpolator); + mCropRectHeight = new FloatProp(prop.cropHeightStart, prop.cropHeightEnd, + mOpeningInterpolator); + mDy = new FloatProp(0, prop.dY, mOpeningInterpolator); + mIconScaleToFitScreen = new FloatProp(prop.initialAppIconScale, + prop.finalAppIconScale, mOpeningInterpolator); + float interpolatedPercent = mOpeningInterpolator.getInterpolation(percent); + mCropRectHeight.value = Utilities.mapRange(interpolatedPercent, + prop.cropHeightStart, prop.cropHeightEnd); + mCropRectCenterY.value = Utilities.mapRange(interpolatedPercent, + prop.cropCenterYStart, prop.cropCenterYEnd); + mDy.value = Utilities.mapRange(interpolatedPercent, 0, prop.dY); + mIconScaleToFitScreen.value = Utilities.mapRange(interpolatedPercent, + prop.initialAppIconScale, prop.finalAppIconScale); + } + // Calculate the size of the scaled icon. float iconWidth = launcherIconBounds.width() * mIconScaleToFitScreen.value; float iconHeight = launcherIconBounds.height() * mIconScaleToFitScreen.value; @@ -801,8 +884,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener final int windowCropWidth = crop.width(); final int windowCropHeight = crop.height(); if (rotationChange != 0) { - Utilities.rotateBounds(crop, mDeviceProfile.widthPx, - mDeviceProfile.heightPx, rotationChange); + Utilities.rotateBounds(crop, mDeviceProfile.getDeviceProperties().getWidthPx(), + mDeviceProfile.getDeviceProperties().getHeightPx(), rotationChange); } // Scale the size of the icon to match the size of the window crop. @@ -849,14 +932,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener matrix.setScale(scale, scale); if (rotationChange == 1) { matrix.postTranslate(windowTransY0, - mDeviceProfile.widthPx - (windowTransX0 + scaledCropWidth)); + mDeviceProfile.getDeviceProperties().getWidthPx() - (windowTransX0 + scaledCropWidth)); } else if (rotationChange == 2) { matrix.postTranslate( - mDeviceProfile.widthPx - (windowTransX0 + scaledCropWidth), - mDeviceProfile.heightPx - (windowTransY0 + scaledCropHeight)); + mDeviceProfile.getDeviceProperties().getWidthPx() - (windowTransX0 + scaledCropWidth), + mDeviceProfile.getDeviceProperties().getHeightPx() - (windowTransY0 + scaledCropHeight)); } else if (rotationChange == 3) { matrix.postTranslate( - mDeviceProfile.heightPx - (windowTransY0 + scaledCropHeight), + mDeviceProfile.getDeviceProperties().getHeightPx() - (windowTransY0 + scaledCropHeight), windowTransX0); } else { matrix.postTranslate(windowTransX0, windowTransY0); @@ -924,9 +1007,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v, - RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets, - RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) { + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) { Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets)); boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets); @@ -948,7 +1031,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener FloatingWidgetView.getDefaultBackgroundColor(mLauncher, openingTarget); } - final float finalWindowRadius = mDeviceProfile.isMultiWindowMode + final float finalWindowRadius = mDeviceProfile.getDeviceProperties().isMultiWindowMode() ? 0 : getWindowCornerRadius(mLauncher); final FloatingWidgetView floatingView = FloatingWidgetView.getFloatingWidgetView(mLauncher, v, widgetBackgroundBounds, @@ -1063,18 +1146,23 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return animatorSet; } - /** - * Returns animator that controls depth/blur of the background. - */ - private ObjectAnimator getBackgroundAnimator() { + /** Returns animator that controls depth/blur of the background during app/widget opening. */ + private Animator getBackgroundAnimator() { + if (Flags.allAppsBlur()) { + // Don't animate/blur the background for this launch, regardless of the launcher state. + // We have too many performance issues with the blur. + return new AnimatorSet(); + } + // When launching an app from overview that doesn't map to a task, we still want to just // blur the wallpaper instead of the launcher surface as well - boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW - && BlurUtils.supportsBlursOnWindows(); + LauncherState launcherState = mLauncher.getStateManager().getState(); + boolean allowBlurringLauncher = + launcherState != OVERVIEW && BlurUtils.supportsBlursOnWindows(); - LaunchDepthController depthController = new LaunchDepthController(mLauncher); - ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController.stateDepth, - MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(mLauncher)) + ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat( + mLauncher.getDepthController().stateDepth, MULTI_PROPERTY_VALUE, + BACKGROUND_APP.getDepth(mLauncher)) .setDuration(APP_LAUNCH_DURATION); if (allowBlurringLauncher) { @@ -1096,18 +1184,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener .setEffectLayer() .build(); - backgroundRadiusAnim.addListener(AnimatorListeners.forEndCallback(() -> - new SurfaceControl.Transaction().remove(dimLayer).apply())); + backgroundRadiusAnim.addListener(AnimatorListeners.forEndCallback(() -> { + // Use try-with-resources to ensure the transaction gets closed. + try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { + transaction.remove(dimLayer).apply(); + } + })); } - backgroundRadiusAnim.addListener( - AnimatorListeners.forEndCallback(() -> { - // reset the depth to match the main depth controller's depth - depthController.stateDepth - .setValue(mLauncher.getDepthController().stateDepth.getValue()); - depthController.dispose(); - })); - return backgroundRadiusAnim; } @@ -1123,7 +1207,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener try { mLauncher.registerRemoteAnimations(definition); } catch (Throwable t) { - // Ignore + // LC-Ignored } } @@ -1132,39 +1216,25 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * additional animations. */ private void addRemoteAnimations(RemoteAnimationDefinition definition) { - mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */); + mWallpaperOpenRunner = new WallpaperOpenLauncherAnimationRunner(); definition.addRemoteAnimation(WindowManager.TRANSIT_OLD_WALLPAPER_OPEN, WindowConfiguration.ACTIVITY_TYPE_STANDARD, new RemoteAnimationAdapter( new LauncherAnimationRunner(mHandler, mWallpaperOpenRunner, false /* startAtFrontOfQueue */), CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); - - if (KEYGUARD_ANIMATION.get()) { - mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */); - definition.addRemoteAnimation( - WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER, - new RemoteAnimationAdapter( - new LauncherAnimationRunner( - mHandler, mKeyguardGoingAwayRunner, - true /* startAtFrontOfQueue */), - CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); - } } /** * Registers remote animations used when closing apps to home screen. */ public void registerRemoteTransitions() { - if (SEPARATE_RECENTS_ACTIVITY.get() && !Utilities.ATLEAST_T) { + SystemUiProxy.INSTANCE.get(mLauncher).shareTransactionQueue(); + if (SEPARATE_RECENTS_ACTIVITY.get()) { return; } - if (ENABLE_SHELL_TRANSITIONS) { - SystemUiProxy.INSTANCE.get(mLauncher).shareTransactionQueue(); - } - - mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */); + mWallpaperOpenTransitionRunner = new WallpaperOpenLauncherAnimationRunner(); mLauncherOpenTransition = LawnchairQuickstepCompat.getRemoteTransitionCompat().getRemoteTransition( new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner, false /* startAtFrontOfQueue */).toRemoteTransition(), @@ -1173,20 +1243,32 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener TransitionFilter homeCheck = new TransitionFilter(); // No need to handle the transition that also dismisses keyguard. homeCheck.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; + homeCheck.mRequirements = new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(), + new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; + homeCheck.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME; homeCheck.mRequirements[0].mTopActivity = mLauncher.getComponentName(); homeCheck.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; homeCheck.mRequirements[0].mOrder = CONTAINER_ORDER_TOP; + homeCheck.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD; homeCheck.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; + + homeCheck.mRequirements[2].mNot = true; + homeCheck.mRequirements[2].mCustomAnimation = true; + homeCheck.mRequirements[2].mMustBeTask = true; + homeCheck.mRequirements[2].mMustBeIndependent = true; + SystemUiProxy.INSTANCE.get(mLauncher) .registerRemoteTransition(mLauncherOpenTransition, homeCheck); if (mBackAnimationController != null) { mBackAnimationController.registerComponentCallbacks(); - mBackAnimationController.registerBackCallbacks(mHandler); + if (isHomeRoleHeld()) { + mBackAnimationController.registerBackCallbacks(mHandler); + } } } @@ -1195,8 +1277,28 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener unregisterRemoteTransitions(); mLauncher.removeOnDeviceProfileChangeListener(this); SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null); - ORDERED_BG_EXECUTOR.execute(() -> mLauncher.getContentResolver() - .unregisterContentObserver(mAnimationRemovalObserver)); + if (BuildConfigs.IS_STUDIO_BUILD && !mRegisteredTaskStackChangeListener.isEmpty()) { + Log.e(TAG, "IllegalState: Failed to run onEndCallback created from" + + " getActivityLaunchOptions()"); + } + mRegisteredTaskStackChangeListener.forEach(TaskRestartedDuringLaunchListener::unregister); + mRegisteredTaskStackChangeListener.clear(); + } + + /** + * Called when the overview-target changes. Updates the back callback registration state. + */ + public void onOverviewTargetChange() { + if (isHomeRoleHeld()) { + mBackAnimationController.registerBackCallbacks(mHandler); + } else { + mBackAnimationController.unregisterBackCallbacks(); + } + } + + private boolean isHomeRoleHeld() { + RoleManager roleManager = mLauncher.getSystemService(RoleManager.class); + return roleManager == null || roleManager.isRoleHeld(ROLE_HOME); } private void unregisterRemoteAnimations() { @@ -1209,13 +1311,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // definition so we don't have to wait for the system gc mWallpaperOpenRunner = null; mAppLaunchRunner = null; - mKeyguardGoingAwayRunner = null; } protected void unregisterRemoteTransitions() { - if (ENABLE_SHELL_TRANSITIONS) { - SystemUiProxy.INSTANCE.get(mLauncher).unshareTransactionQueue(); - } + SystemUiProxy.INSTANCE.get(mLauncher).unshareTransactionQueue(); if (SEPARATE_RECENTS_ACTIVITY.get()) { return; } @@ -1231,17 +1330,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - private void checkAndMonitorIfAnimationsAreEnabled() { - ORDERED_BG_EXECUTOR.execute(() -> { - mAnimationRemovalObserver.onChange(true); - mLauncher.getContentResolver().registerContentObserver(Global.getUriFor( - Global.ANIMATOR_DURATION_SCALE), false, mAnimationRemovalObserver); - mLauncher.getContentResolver().registerContentObserver(Global.getUriFor( - Global.TRANSITION_ANIMATION_SCALE), false, mAnimationRemovalObserver); - - }); - } - private boolean launcherIsATargetWithMode(RemoteAnimationTarget[] targets, int mode) { for (RemoteAnimationTarget target : targets) { if (target.mode == mode && target.taskInfo != null @@ -1269,42 +1357,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return false; } - /** - * @return Runner that plays when user goes to Launcher - * ie. pressing home, swiping up from nav bar. - */ - RemoteAnimationFactory createWallpaperOpenRunner(boolean fromUnlock) { - return new WallpaperOpenLauncherAnimationRunner(fromUnlock); - } - - /** - * Animator that controls the transformations of the windows when unlocking the device. - */ - private Animator getUnlockWindowAnimator(RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets) { - SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer); - ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1); - unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS); - float cornerRadius = mDeviceProfile.isMultiWindowMode ? 0 : - QuickStepContract.getWindowCornerRadius(mLauncher); - unlockAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - SurfaceTransaction transaction = new SurfaceTransaction(); - for (int i = appTargets.length - 1; i >= 0; i--) { - RemoteAnimationTarget target = appTargets[i]; - transaction.forSurface(target.leash) - .setAlpha(1f) - .setWindowCrop(target.screenSpaceBounds) - .setCornerRadius(cornerRadius); - } - surfaceApplier.scheduleApply(transaction); - } - }); - return unlockAnimator; - } - - public static int getRotationChange(RemoteAnimationTarget[] appTargets) { + private static int getRotationChange(RemoteAnimationTarget[] appTargets) { int rotationChange = 0; for (RemoteAnimationTarget target : appTargets) { // LC: https://github.com/LawnchairLauncher/lawnchair/pull/3776 @@ -1361,13 +1414,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Find the associated item info for the launch cookie (if available), note that predicted // apps actually have an id of -1, so use another default id here - final ArrayList launchCookies = runningTaskTarget.taskInfo.launchCookies == null - ? new ArrayList<>() + final List launchCookies = runningTaskTarget.taskInfo.launchCookies == null + ? Collections.EMPTY_LIST : runningTaskTarget.taskInfo.launchCookies; - return mLauncher.getFirstMatchForAppClose( + return mLauncher.getFirstVisibleElementForAppClose( StableViewInfo.fromLaunchCookies(launchCookies), packageName, - UserHandle.of(runningTaskTarget.taskInfo.userId), true /* supportsAllAppsState */); + UserHandle.of(runningTaskTarget.taskInfo.userId)); } private @NonNull RectF getDefaultWindowTargetRect() { @@ -1376,9 +1429,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener DeviceProfile dp = mLauncher.getDeviceProfile(); final int halfIconSize = dp.iconSizePx / 2; float primaryDimension = orientationHandler - .getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx); + .getPrimaryValue(dp.getDeviceProperties().getAvailableWidthPx(), dp.getDeviceProperties().getAvailableHeightPx()); float secondaryDimension = orientationHandler - .getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx); + .getSecondaryValue(dp.getDeviceProperties().getAvailableWidthPx(), dp.getDeviceProperties().getAvailableHeightPx()); final float targetX = primaryDimension / 2f; final float targetY = secondaryDimension - dp.hotseatBarSizePx; return new RectF(targetX - halfIconSize, targetY - halfIconSize, @@ -1389,8 +1442,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * Closing animator that animates the window into its final location on the workspace. */ protected RectFSpringAnim getClosingWindowAnimators(AnimatorSet animation, - RemoteAnimationTarget[] targets, View launcherView, PointF velocityPxPerS, - RectF closingWindowStartRectF, float startWindowCornerRadius) { + RemoteAnimationTarget[] targets, View launcherView, PointF velocityPxPerS, + RectF closingWindowStartRectF, float startWindowCornerRadius) { FloatingIconView floatingIconView = null; FloatingWidgetView floatingWidget = null; RectF targetRect = new RectF(); @@ -1408,22 +1461,28 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Get floating view and target rect. boolean isInHotseat = false; if (launcherView instanceof LauncherAppWidgetHostView) { - Size windowSize = new Size(mDeviceProfile.availableWidthPx, - mDeviceProfile.availableHeightPx); + Size windowSize = new Size(mDeviceProfile.getDeviceProperties().getAvailableWidthPx(), + mDeviceProfile.getDeviceProperties().getAvailableHeightPx()); int fallbackBackgroundColor = FloatingWidgetView.getDefaultBackgroundColor(mLauncher, runningTaskTarget); floatingWidget = FloatingWidgetView.getFloatingWidgetView(mLauncher, (LauncherAppWidgetHostView) launcherView, targetRect, windowSize, - mDeviceProfile.isMultiWindowMode ? 0 : getWindowCornerRadius(mLauncher), + mDeviceProfile.getDeviceProperties().isMultiWindowMode() ? 0 : getWindowCornerRadius(mLauncher), isTransluscent, fallbackBackgroundColor); - } else if (launcherView != null && mAreAnimationsEnabled) { + } else if (launcherView != null && !RemoveAnimationSettingsTracker.INSTANCE.get( + mLauncher).isRemoveAnimationEnabled()) { floatingIconView = getFloatingIconView(mLauncher, launcherView, null, mLauncher.getTaskbarUIController() == null ? null : mLauncher.getTaskbarUIController().findMatchingView(launcherView), true /* hideOriginal */, targetRect, false /* isOpening */); - isInHotseat = launcherView.getTag() instanceof ItemInfo - && ((ItemInfo) launcherView.getTag()).isInHotseat(); + if (launcherView.getTag() instanceof ItemInfo itemInfo) { + isInHotseat = itemInfo.isInHotseat(); + if (isInHotseat) { + int dx = mLauncher.getHotseatItemTranslationX(itemInfo); + targetRect.offset(dx, 0); + } + } } else { targetRect.set(getDefaultWindowTargetRect()); } @@ -1432,14 +1491,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener RectFSpringAnim anim = new RectFSpringAnim(useTaskbarHotseatParams ? new TaskbarHotseatSpringConfig(mLauncher, closingWindowStartRectF, targetRect) : new DefaultSpringConfig(mLauncher, mDeviceProfile, closingWindowStartRectF, - targetRect)); + targetRect)); // Hook up floating views to the closing window animators. // note the coordinate of closingWindowStartRect is based on launcher Rect closingWindowStartRect = new Rect(); closingWindowStartRectF.round(closingWindowStartRect); Rect closingWindowOriginalRect = - new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); + new Rect(0, 0, mDeviceProfile.getDeviceProperties().getWidthPx(), mDeviceProfile.getDeviceProperties().getHeightPx()); if (floatingIconView != null) { anim.addAnimatorListener(floatingIconView); floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged); @@ -1447,15 +1506,20 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener FloatingIconView finalFloatingIconView = floatingIconView; // We want the window alpha to be 0 once this threshold is met, so that the - // FolderIconView can be seen morphing into the icon shape. + // FloatingIconView can be seen morphing into the icon shape. final float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION; RectFSpringAnim.OnUpdateListener runner = new SpringAnimRunner(targets, targetRect, closingWindowStartRect, closingWindowOriginalRect, startWindowCornerRadius) { @Override public void onUpdate(RectF currentRectF, float progress) { - finalFloatingIconView.update(1f, currentRectF, progress, windowAlphaThreshold, - getCornerRadius(progress), false); + // We want the icon alpha to be 1 once this threshold is met, so that it can be + // seen morphing into the icon shape. But before the threshold, we want to limit + // the alpha to reduce the blur effect behind the window. + float iconAlpha = + Interpolators.clampToProgress(progress, 0f, windowAlphaThreshold); + finalFloatingIconView.update(iconAlpha, currentRectF, progress, + windowAlphaThreshold, getCornerRadius(progress), false); super.onUpdate(currentRectF, progress); } @@ -1512,14 +1576,20 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener Rect tmpRect = new Rect(); ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1); int duration = CLOSING_TRANSITION_DURATION_MS; - float windowCornerRadius = mDeviceProfile.isMultiWindowMode + float windowCornerRadius = mDeviceProfile.getDeviceProperties().isMultiWindowMode() ? 0 : getWindowCornerRadius(mLauncher); float startShadowRadius = areAllTargetsTranslucent(appTargets) ? 0 : mMaxShadowRadius; closingAnimator.setDuration(duration); + boolean isFreeform = isFreeformAnimation(appTargets); + float translateY = isFreeform ? mClosingFreeformWindowTransY : mClosingWindowTransY; + float endScale = isFreeform ? 0.95f : 1f; + Interpolator alphaInterpolator = isFreeform + ? clampToDuration(LINEAR, 0, 100, duration) + : clampToDuration(LINEAR, 25, 125, duration); closingAnimator.addUpdateListener(new MultiValueUpdateListener() { - FloatProp mDy = new FloatProp(0, mClosingWindowTransY, DECELERATE_1_7); - FloatProp mScale = new FloatProp(1f, 1f, DECELERATE_1_7); - FloatProp mAlpha = new FloatProp(1f, 0f, clampToDuration(LINEAR, 25, 125, duration)); + FloatProp mDy = new FloatProp(0, translateY, DECELERATE_1_7); + FloatProp mScale = new FloatProp(1f, endScale, DECELERATE_1_7); + FloatProp mAlpha = new FloatProp(1f, 0f, alphaInterpolator); FloatProp mShadowRadius = new FloatProp(startShadowRadius, 0, DECELERATE_1_7); @Override @@ -1568,8 +1638,29 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return closingAnimator; } + private boolean isFreeformAnimation(RemoteAnimationTarget[] appTargets) { + return DesktopModeStatus.canEnterDesktopMode(mLauncher.getApplicationContext()) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX.isTrue() + && Arrays.stream(appTargets) + .anyMatch(app -> app.taskInfo != null && app.taskInfo.isFreeform()); + } + private void addCujInstrumentation(Animator anim, int cuj) { - anim.addListener(new AnimationSuccessListener() { + anim.addListener(getCujAnimationSuccessListener(cuj, /* cujPreStartCallback= */null)); + } + + private void addCujInstrumentation(Animator anim, int cuj, Runnable cujPreStartCallback) { + anim.addListener(getCujAnimationSuccessListener(cuj, cujPreStartCallback)); + } + + private void addCujInstrumentation(RectFSpringAnim anim, int cuj) { + anim.addAnimatorListener( + getCujAnimationSuccessListener(cuj, /* cujPreStartCallback= */null)); + } + + private AnimationSuccessListener getCujAnimationSuccessListener( + int cuj, Runnable cujPreStartCallback) { + return new AnimationSuccessListener() { @Override public void onAnimationStart(Animator animation) { mDragLayer.getViewTreeObserver().addOnDrawListener( @@ -1582,7 +1673,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return; } mHandled = true; - + if (cujPreStartCallback != null) { + cujPreStartCallback.run(); + } InteractionJankMonitorWrapper.begin(mDragLayer, cuj); mDragLayer.post(() -> @@ -1603,38 +1696,66 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public void onAnimationSuccess(Animator animator) { InteractionJankMonitorWrapper.end(cuj); } - }); + }; } /** * Creates the {@link RectFSpringAnim} and {@link AnimatorSet} required to animate * the transition. */ - public Pair createWallpaperOpenAnimations( + @NonNull + public BackAnimState createWallpaperOpenAnimations( RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets, - boolean fromUnlock, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonAppTargets, RectF startRect, float startWindowCornerRadius, boolean fromPredictiveBack) { + View launcherView = findLauncherView(appTargets); + if (checkReturnAnimationsFlags() + && launcherView != null + && launcherView.getTag() instanceof ItemInfo info + && info.shouldUseBackgroundAnimation()) { + // Try to create a return animation + RunnableList onEndCallback = new RunnableList(); + WindowAnimationState windowState = new WindowAnimationState(); + windowState.bounds = startRect; + windowState.bottomLeftRadius = windowState.bottomRightRadius = + windowState.topLeftRadius = windowState.topRightRadius = + startWindowCornerRadius; + ContainerAnimationRunner runner = ContainerAnimationRunner.fromView( + launcherView, false /* forLaunch */, mLauncher, mStartingWindowListener, + onEndCallback, windowState); + if (runner != null) { + runner.startAnimation(TRANSIT_CLOSE, + appTargets, wallpapers, nonAppTargets, + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() { + onEndCallback.executeAllAndDestroy(); + } + }); + return new AlreadyStartedBackAnimState(onEndCallback); + } + } + AnimatorSet anim = new AnimatorSet(); RectFSpringAnim rectFSpringAnim = null; final boolean launcherIsForceInvisibleOrOpening = mLauncher.isForceInvisible() || launcherIsATargetWithMode(appTargets, MODE_OPENING); - View launcherView = findLauncherView(appTargets); boolean playFallBackAnimation = (launcherView == null && launcherIsForceInvisibleOrOpening) || mLauncher.getWorkspace().isOverlayShown() || shouldPlayFallbackClosingAnimation(appTargets); - boolean playWorkspaceReveal = !fromPredictiveBack; + boolean playWorkspaceReveal = true; + if (!Flags.predictiveBackToHomePolish()) { + playWorkspaceReveal = !fromPredictiveBack; + } boolean skipAllAppsScale = false; - if (fromUnlock) { - anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets)); - } else if (ENABLE_BACK_SWIPE_HOME_ANIMATION.get() - && !playFallBackAnimation) { + if (!playFallBackAnimation) { PointF velocity; if (enableScalingRevealHomeAnimation()) { velocity = new PointF(); @@ -1651,23 +1772,17 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Skip scaling all apps, otherwise FloatingIconView will get wrong // layout bounds. skipAllAppsScale = true; - } else if (!fromPredictiveBack) { + } else if (Flags.predictiveBackToHomePolish() || !fromPredictiveBack) { if (enableScalingRevealHomeAnimation()) { anim.play( - new ScalingWorkspaceRevealAnim( - mLauncher, rectFSpringAnim, - rectFSpringAnim.getTargetRect()).getAnimators()); + new ScalingWorkspaceRevealAnim(mLauncher, rectFSpringAnim, + rectFSpringAnim.getTargetRect(), + !fromPredictiveBack /* playAlphaReveal */).getAnimators()); } else { anim.play(new StaggeredWorkspaceAnim(mLauncher, velocity.y, true /* animateOverviewScrim */, launcherView).getAnimators()); } - if (!areAllTargetsTranslucent(appTargets)) { - anim.play(ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth, - MULTI_PROPERTY_VALUE, - BACKGROUND_APP.getDepth(mLauncher), NORMAL.getDepth(mLauncher))); - } - // We play StaggeredWorkspaceAnim as a part of the closing window animation. playWorkspaceReveal = false; } @@ -1675,6 +1790,22 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener anim.play(getFallbackClosingWindowAnimators(appTargets)); } + if (Flags.predictiveBackToHomePolish()) { + AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + AccessibilityManagerCompat.sendTestProtocolEventToTest( + mLauncher, WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE); + } + }; + if (rectFSpringAnim != null) { + rectFSpringAnim.addAnimatorListener(endListener); + } else { + anim.addListener(endListener); + } + } + // Normally, we run the launcher content animation when we are transitioning // home, but if home is already visible, then we don't want to animate the // contents of launcher unless we know that we are animating home as a result @@ -1683,30 +1814,45 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // targets list because it is already visible). In that case, we force // invisibility on touch down, and only reset it after the animation to home // is initialized. - if (launcherIsForceInvisibleOrOpening || fromPredictiveBack) { - addCujInstrumentation(anim, playFallBackAnimation - ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK - : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME); - - AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - AccessibilityManagerCompat.sendTestProtocolEventToTest( - mLauncher, WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE); - } - }; - - if (fromPredictiveBack && rectFSpringAnim != null) { - rectFSpringAnim.addAnimatorListener(endListener); + boolean legacyFromPredictiveBack = + !Flags.predictiveBackToHomePolish() && fromPredictiveBack; + if (launcherIsForceInvisibleOrOpening || legacyFromPredictiveBack) { + if (rectFSpringAnim != null && anim.getChildAnimations().isEmpty()) { + addCujInstrumentation(rectFSpringAnim, Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME); } else { - anim.addListener(endListener); + if (isFreeformAnimation(appTargets)) { + addCujInstrumentation( + anim, + Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE, + /* cujPreStartCallback= */ () -> { + mLatencyTracker.onActionEnd( + ACTION_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE); + }); + } + addCujInstrumentation(anim, playFallBackAnimation + ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK + : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME); + } + if (!Flags.predictiveBackToHomePolish()) { + AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + AccessibilityManagerCompat.sendTestProtocolEventToTest( + mLauncher, WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE); + } + }; + if (fromPredictiveBack && rectFSpringAnim != null) { + rectFSpringAnim.addAnimatorListener(endListener); + } else { + anim.addListener(endListener); + } } // Only register the content animation for cancellation when state changes mLauncher.getStateManager().setCurrentAnimation(anim); - if (mLauncher.isInState(LauncherState.ALL_APPS) && !fromPredictiveBack) { + if (mLauncher.isInState(LauncherState.ALL_APPS) && !legacyFromPredictiveBack) { Pair contentAnimator = getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY, skipAllAppsScale); @@ -1722,34 +1868,45 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - return new Pair(rectFSpringAnim, anim); + return new AnimatorBackState(rectFSpringAnim, anim); } - public static int getTaskbarToHomeDuration() { - if (enableScalingRevealHomeAnimation()) { + /** Get animation duration for taskbar for going to home. */ + public static int getTaskbarToHomeDuration(boolean isPinnedTaskbarAndNotInDesktopMode) { + return getTaskbarToHomeDuration(false, isPinnedTaskbarAndNotInDesktopMode); + } + + /** + * Get animation duration for taskbar for going to home. + * + * @param shouldOverrideToFastAnimation should overwrite scaling reveal home animation duration + */ + public static int getTaskbarToHomeDuration(boolean shouldOverrideToFastAnimation, + boolean isPinnedTaskbarAndNotInDesktopMode) { + if (isPinnedTaskbarAndNotInDesktopMode) { + return PINNED_TASKBAR_TRANSITION_DURATION; + } else if (enableScalingRevealHomeAnimation() && !shouldOverrideToFastAnimation) { return TASKBAR_TO_HOME_DURATION_SLOW; } else { return TASKBAR_TO_HOME_DURATION_FAST; } } + private static boolean checkReturnAnimationsFlags() { + return enableContainerReturnAnimations() && returnAnimationFrameworkLibrary(); + } + /** * Remote animation runner for animation from the app to Launcher, including recents. */ protected class WallpaperOpenLauncherAnimationRunner implements RemoteAnimationFactory { - private final boolean mFromUnlock; - - public WallpaperOpenLauncherAnimationRunner(boolean fromUnlock) { - mFromUnlock = fromUnlock; - } - @Override public void onAnimationStart(int transit, - RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets, - RemoteAnimationTarget[] nonAppTargets, - LauncherAnimationRunner.AnimationResult result) { + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, + LauncherAnimationRunner.AnimationResult result) { if (mLauncher.isDestroyed()) { AnimatorSet anim = new AnimatorSet(); anim.play(getFallbackClosingWindowAnimators(appTargets)); @@ -1774,14 +1931,14 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - Pair pair = createWallpaperOpenAnimations( - appTargets, wallpaperTargets, mFromUnlock, resolveRectF, + BackAnimState bankAnimState = createWallpaperOpenAnimations( + appTargets, wallpaperTargets, nonAppTargets, resolveRectF, QuickStepContract.getWindowCornerRadius(mLauncher), false /* fromPredictiveBack */); TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets, false, null); mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); - result.setAnimation(pair.second, mLauncher); + bankAnimState.applyToAnimationResult(result, mLauncher); } } @@ -1800,10 +1957,10 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener @Override public void onAnimationStart(int transit, - RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets, - RemoteAnimationTarget[] nonAppTargets, - LauncherAnimationRunner.AnimationResult result) { + RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, + RemoteAnimationTarget[] nonAppTargets, + LauncherAnimationRunner.AnimationResult result) { AnimatorSet anim = new AnimatorSet(); boolean launcherClosing = launcherIsATargetWithMode(appTargets, MODE_CLOSING); @@ -1833,6 +1990,21 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener anim.addListener(mForceInvisibleListener); } + // Syncs the app launch animation and taskbar stash animation (if exists). + if (syncAppLaunchWithTaskbarStash() && enableScalingRevealHomeAnimation()) { + LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController(); + if (taskbarController != null) { + taskbarController.setIgnoreInAppFlagForSync(false); + + if (launcherClosing) { + Animator taskbar = taskbarController.createAnimToApp(); + if (taskbar != null) { + anim.play(taskbar); + } + } + } + } + result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy, skipFirstFrame); } @@ -1855,32 +2027,29 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } @Nullable - private static ContainerAnimationRunner from(View v, Launcher launcher, - StartingWindowListener startingWindowListener, RunnableList onEndCallback) { - View viewToUse = findLaunchableViewWithBackground(v); - if (viewToUse == null) { - return null; + static ContainerAnimationRunner fromView( + View v, + boolean forLaunch, + Launcher launcher, + StartingWindowListener startingWindowListener, + RunnableList onEndCallback, + @Nullable WindowAnimationState windowState) { + if (!forLaunch && !checkReturnAnimationsFlags()) { + throw new IllegalStateException( + "forLaunch cannot be false when the enableContainerReturnAnimations or " + + "returnAnimationFrameworkLibrary flag is disabled"); } - // The CUJ is logged by the click handler, so we don't log it inside the animation - // library. - ActivityTransitionAnimator.Controller controllerDelegate = - ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */); - - if (controllerDelegate == null) { - return null; - } - - // This wrapper allows us to override the default value, telling the controller that the - // current window is below the animating window. + // First the controller is created. This is used by the runner to animate the + // origin/target view. ActivityTransitionAnimator.Controller controller = - new DelegateTransitionAnimatorController(controllerDelegate) { - @Override - public boolean isBelowAnimatingWindow() { - return true; - } - }; + buildController(v, forLaunch, windowState); + if (controller == null) { + return null; + } + // The callback is used to make sure that we use the right color to fade between view + // and the window. ActivityTransitionAnimator.Callback callback = task -> { final int backgroundColor = startingWindowListener.mBackgroundColor == Color.TRANSPARENT @@ -1902,6 +2071,50 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener MAIN_EXECUTOR, controller, callback, listener)); } + /** + * Constructs a {@link ActivityTransitionAnimator.Controller} that can be used by a + * {@link ContainerAnimationRunner} to animate a view into an opening window or from a + * closing one. + */ + @Nullable + private static ActivityTransitionAnimator.Controller buildController( + View v, boolean isLaunching, @Nullable WindowAnimationState windowState) { + View viewToUse = findLaunchableViewWithBackground(v); + if (viewToUse == null) { + return null; + } + + // The CUJ is logged by the click handler, so we don't log it inside the animation + // library. TODO: figure out return CUJ. + ActivityTransitionAnimator.Controller controllerDelegate = + ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */); + + if (controllerDelegate == null) { + return null; + } + + // This wrapper allows us to override the default value, telling the controller that the + // current window is below the animating window as well as information about the return + // animation. + return new DelegateTransitionAnimatorController(controllerDelegate) { + @Override + public boolean isLaunching() { + return isLaunching; + } + + @Override + public boolean isBelowAnimatingWindow() { + return true; + } + + @Nullable + @Override + public WindowAnimationState getWindowAnimatorState() { + return windowState; + } + }; + } + /** * Finds the closest parent of [view] (inclusive) that implements {@link LaunchableView} and * has a background drawable. @@ -1911,20 +2124,26 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener View view) { View current = view; while (current.getBackground() == null || !(current instanceof LaunchableView)) { - if (!(current.getParent() instanceof View)) { + if (current.getParent() instanceof View v) { + current = v; + } else { return null; } - - current = (View) current.getParent(); } - return (T) current; } @Override public void onAnimationStart(int transit, RemoteAnimationTarget[] appTargets, - RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets, - LauncherAnimationRunner.AnimationResult result) { + RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets, + LauncherAnimationRunner.AnimationResult result) { + startAnimation( + transit, appTargets, wallpaperTargets, nonAppTargets, result); + } + + public void startAnimation(int transit, RemoteAnimationTarget[] appTargets, + RemoteAnimationTarget[] wallpaperTargets, RemoteAnimationTarget[] nonAppTargets, + IRemoteAnimationFinishedCallback result) { mDelegate.onAnimationStart( transit, appTargets, wallpaperTargets, nonAppTargets, result); } @@ -1959,8 +2178,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener public final float iconAlphaStart; AnimOpenProperties(Resources r, DeviceProfile dp, Rect windowTargetBounds, - RectF launcherIconBounds, View view, int dragLayerLeft, int dragLayerTop, - boolean hasSplashScreen, boolean hasDifferentAppIcon) { + RectF launcherIconBounds, View view, int dragLayerLeft, int dragLayerTop, + boolean hasSplashScreen, boolean hasDifferentAppIcon) { // Scale the app icon to take up the entire screen. This simplifies the math when // animating the app window position / scale. float smallestSize = Math.min(windowTargetBounds.height(), windowTargetBounds.width()); @@ -2029,7 +2248,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * target. */ public void transferRectToTargetCoordinate(RemoteAnimationTarget target, RectF currentRect, - boolean toLauncher, RectF resultRect) { + boolean toLauncher, RectF resultRect) { mCoordinateTransfer.transferRectToTargetCoordinate( target, currentRect, toLauncher, resultRect); } @@ -2044,20 +2263,23 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } public void transferRectToTargetCoordinate(RemoteAnimationTarget target, RectF currentRect, - boolean toLauncher, RectF resultRect) { + boolean toLauncher, RectF resultRect) { final int taskRotation = target.windowConfiguration.getRotation(); final DeviceProfile profile = mLauncher.getDeviceProfile(); + final int rotation = profile.getDeviceProperties().getRotationHint(); + final int widthPx = profile.getDeviceProperties().getWidthPx(); + final int heightPx = profile.getDeviceProperties().getWidthPx(); final int rotationDelta = toLauncher - ? android.util.RotationUtils.deltaRotation(taskRotation, profile.rotationHint) - : android.util.RotationUtils.deltaRotation(profile.rotationHint, taskRotation); + ? android.util.RotationUtils.deltaRotation(taskRotation, rotation) + : android.util.RotationUtils.deltaRotation(rotation, taskRotation); if (rotationDelta != ROTATION_0) { // Get original display size when task is on top but with different rotation - if (rotationDelta % 2 != 0 && toLauncher && (profile.rotationHint == ROTATION_0 - || profile.rotationHint == ROTATION_180)) { - mDisplayRect.set(0, 0, profile.heightPx, profile.widthPx); + if (rotationDelta % 2 != 0 && toLauncher && (rotation == ROTATION_0 + || rotation == ROTATION_180)) { + mDisplayRect.set(0, 0, heightPx, widthPx); } else { - mDisplayRect.set(0, 0, profile.widthPx, profile.heightPx); + mDisplayRect.set(0, 0, widthPx, heightPx); } currentRect.round(mTmpResult); android.util.RotationUtils.rotateBounds(mTmpResult, mDisplayRect, rotationDelta); @@ -2097,8 +2319,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener * @param startWindowCornerRadius corner radius of window at the start position */ public SpringAnimRunner(RemoteAnimationTarget[] appTargets, RectF targetRect, - Rect closingWindowStartRect, Rect closingWindowOriginalRect, - float startWindowCornerRadius) { + Rect closingWindowStartRect, Rect closingWindowOriginalRect, + float startWindowCornerRadius) { mAppTargets = appTargets; mStartRadius = startWindowCornerRadius; mEndRadius = Math.max(1, targetRect.width()) / 2f; @@ -2196,19 +2418,4 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener return Utilities.mapToRange(progress, start, end, 1, 0, ACCELERATE_1_5); } } - - public static class LaunchDepthController extends DepthController { - public LaunchDepthController(QuickstepLauncher launcher) { - super(launcher); - try { - setCrossWindowBlursEnabled( - CrossWindowBlurListeners.getInstance().isCrossWindowBlurEnabled()); - } catch (Throwable t) { - // ignore - } - // Make sure that the starting value matches the current depth set by the main - // controller. - stateDepth.setValue(launcher.getDepthController().stateDepth.getValue()); - } - } } diff --git a/quickstep/src/com/android/launcher3/QuickstepWidgetPickerActivity.java b/quickstep/src/com/android/launcher3/QuickstepWidgetPickerActivity.java new file mode 100644 index 0000000000..229ec96bc5 --- /dev/null +++ b/quickstep/src/com/android/launcher3/QuickstepWidgetPickerActivity.java @@ -0,0 +1,550 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT; +import static android.view.WindowInsets.Type.navigationBars; +import static android.view.WindowInsets.Type.statusBars; + +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; +import android.view.WindowInsetsController; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; +import android.window.OnBackInvokedDispatcher; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.compose.ComposeFacade; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherComponentProvider; +import com.android.launcher3.model.StringCache; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.model.WidgetPredictionsRequester; +import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PackageItemInfo; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.picker.WidgetCategoryFilter; +import com.android.launcher3.widget.picker.WidgetsFullSheet; +import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider; +import com.android.launcher3.widgetpicker.WidgetPickerConfig; +import com.android.systemui.animation.back.FlingOnBackAnimationCallback; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** An Activity that can host Launcher's widget picker for additional surfaces. */ +public class QuickstepWidgetPickerActivity extends + com.android.launcher3.widgetpicker.WidgetPickerActivity implements + WidgetPredictionsRequester.WidgetPredictionsListener { + private static final String TAG = "WidgetPickerActivity"; + /** + * Name of the extra that indicates that a widget being dragged. + * + *

When set to "true" in the result of startActivityForResult, the client that launched the + * picker knows that activity was closed due to pending drag. + */ + private static final String EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"; + + // Intent extras that specify the desired widget width and height. If these are not specified in + // the intent, then widgets will not be filtered for size. + private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"; + private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"; + // Unlike the AppWidgetManager.EXTRA_CATEGORY_FILTER, this filter removes certain categories. + // Filter is ignore if it is not a negative value. + // Example usage: WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv() + private static final String EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter"; + /** + * Widgets currently added by the user in the UI surface. + *

This allows widget picker to exclude existing widgets from suggestions.

+ */ + private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets"; + /** + * Intent extra for the string representing the title displayed within the picker header. + */ + private static final String EXTRA_PICKER_TITLE = "picker_title"; + /** + * Intent extra for the string representing the description displayed within the picker header. + */ + private static final String EXTRA_PICKER_DESCRIPTION = "picker_description"; + + /** + * A unique identifier of the surface hosting the widgets; + *

"widgets" is reserved for home screen surface.

+ *

"widgets_hub" is reserved for lockscreen hub surface.

+ */ + private static final String EXTRA_UI_SURFACE = "ui_surface"; + private static final String LOCKSCREEN_WIDGETS_HUB_UI_SURFACE = "widgets_hub"; + private static final Pattern UI_SURFACE_PATTERN = + Pattern.compile("^(widgets|widgets_hub)$"); + /** + * User ids that should be filtered out of the widget lists created by this activity. + */ + private static final String EXTRA_USER_ID_FILTER = "filtered_user_ids"; + + private WidgetsModel mModel; + private StringCache mStringCache; + private WidgetPredictionsRequester mWidgetPredictionsRequester; + private WidgetPickerDataProvider mWidgetPickerDataProvider; + private int mDesiredWidgetWidth; + private int mDesiredWidgetHeight; + private WidgetCategoryFilter mWidgetCategoryInclusionFilter; + private WidgetCategoryFilter mWidgetCategoryExclusionFilter; + // Widgets existing on the host surface. + @NonNull + private List mAddedWidgets = new ArrayList<>(); + @Nullable + private WidgetsFullSheet mWidgetSheet; + + private final Predicate mNoShortcutsFilter = widget -> { + final WidgetAcceptabilityVerdict verdict = + isWidgetAcceptable(widget, /* applySizeFilter=*/ false); + verdict.maybeLogVerdict(); + return verdict.isAcceptable; + }; + private final Predicate mHostSizeAndNoShortcutsFilter = widget -> { + final WidgetAcceptabilityVerdict verdict = + isWidgetAcceptable(widget, /* applySizeFilter=*/ true); + verdict.maybeLogVerdict(); + return verdict.isAcceptable; + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setWidgetPickerConfig(parseIntentExtras()); + + super.onCreate(savedInstanceState); + + if (!Flags.enableWidgetPickerRefactor() || !ComposeFacade.INSTANCE.isComposeAvailable()) { + if (getWidgetPickerConfig().getUiSurface().equals(LOCKSCREEN_WIDGETS_HUB_UI_SURFACE)) { + WindowInsetsController wc = getDragLayer().getWindowInsetsController(); + wc.hide(navigationBars() + statusBars()); + } + + LauncherAppComponent component = LauncherComponentProvider.get(this); + InvariantDeviceProfile idp = component.getIDP(); + mDeviceProfile = idp.getDeviceProfile(this); + mModel = new WidgetsModel(getApplicationContext()); + mWidgetPickerDataProvider = new WidgetPickerDataProvider(); + + refreshAndBindWidgets(); + } + } + + @Override + protected void registerBackDispatcher() { + if (Utilities.ATLEAST_T) { + getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + new BackAnimationCallback()); + } + } + + @NonNull + protected WidgetPickerConfig parseIntentExtras() { + String title = getIntent().getStringExtra(EXTRA_PICKER_TITLE); + String description = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION); + + // A value of 0 for either size means that no filtering will occur in that dimension. If + // both values are 0, then no size filtering will occur. + mDesiredWidgetWidth = + getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_WIDTH, 0); + mDesiredWidgetHeight = + getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0); + + // Defaults to '0' to indicate that there isn't a category filter. + // Negative value indicates it's an exclusion filter (e.g. NOT_KEYGUARD_CATEGORY.inv()) + // Positive value indicates it's inclusion filter (e.g. HOME_SCREEN or KEYGUARD) + // Note: A filter can either be inclusion or exclusion filter; not both. + int inclusionFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0); + if (inclusionFilter < 0) { + Log.w(TAG, "Invalid EXTRA_CATEGORY_FILTER: " + inclusionFilter); + } + mWidgetCategoryInclusionFilter = new WidgetCategoryFilter(max(0, inclusionFilter)); + int exclusionFilter = getIntent().getIntExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, 0); + if (exclusionFilter > 0) { + Log.w(TAG, "Invalid EXTRA_CATEGORY_EXCLUSION_FILTER: " + exclusionFilter); + } + mWidgetCategoryExclusionFilter = new WidgetCategoryFilter(min(0, exclusionFilter)); + + String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE); + String uiSurface = WidgetPickerConfig.HOMESCREEN_WIDGETS_UI_SURFACE; + if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) { + uiSurface = uiSurfaceParam; + } + if (Utilities.ATLEAST_T) { + ArrayList addedWidgets = getIntent().getParcelableArrayListExtra( + EXTRA_ADDED_APP_WIDGETS, AppWidgetProviderInfo.class); + if (addedWidgets != null) { + mAddedWidgets = addedWidgets; + } + } + + List filteredUsers = List.of(); + ArrayList filteredUserIds = getIntent().getIntegerArrayListExtra( + EXTRA_USER_ID_FILTER); + if (filteredUserIds != null) { + filteredUsers = filteredUserIds.stream().map(UserHandle::of).toList(); + } + + return new WidgetPickerConfig( + /*uiSurface=*/ uiSurface, + /*title=*/ title, + /*description=*/ description, + /*categoryInclusionFilter=*/ inclusionFilter, + /*categoryExclusionFilter=*/ exclusionFilter, + /*filteredUsers=*/ filteredUsers); + } + + @NonNull + @Override + public WidgetPickerDataProvider getWidgetPickerDataProvider() { + return mWidgetPickerDataProvider; + } + + @Override + public View.OnClickListener getItemOnClickListener() { + return v -> { + final AppWidgetProviderInfo info = + (v instanceof WidgetCell) ? ((WidgetCell) v).getWidgetItem().widgetInfo : null; + if (info == null || info.provider == null) { + return; + } + + setResult(RESULT_OK, new Intent() + .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider) + .putExtra(Intent.EXTRA_USER, info.getProfile())); + + finish(); + }; + } + + @Override + public View.OnLongClickListener getAllAppsItemLongClickListener() { + return view -> { + if (!(view instanceof WidgetCell widgetCell)) return false; + + if (widgetCell.getWidgetView().getDrawable() == null + && widgetCell.getAppWidgetHostViewPreview() == null) { + // The widget preview hasn't been loaded; so, we abort the drag. + return false; + } + + final AppWidgetProviderInfo info = widgetCell.getWidgetItem().widgetInfo; + if (info == null || info.provider == null) { + return false; + } + + View dragView = widgetCell.getDragAndDropView(); + if (dragView == null) { + return false; + } + + ClipData clipData = new ClipData( + new ClipDescription( + /* label= */ "", // not displayed anywhere; so, set to empty. + new String[]{MIMETYPE_TEXT_INTENT} + ), + new ClipData.Item(new Intent() + .putExtra(Intent.EXTRA_USER, info.getProfile()) + .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider)) + ); + + // Set result indicating activity was closed due a widget being dragged. + setResult(RESULT_OK, new Intent() + .putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true)); + + // DRAG_FLAG_GLOBAL permits dragging data beyond app window. + return dragView.startDragAndDrop( + clipData, + new View.DragShadowBuilder(dragView), + /* myLocalState= */ null, + View.DRAG_FLAG_GLOBAL + ); + }; + } + + /** + * Updates the model with widgets, applies filters and launches the widgets sheet once + * widgets are available + */ + private void refreshAndBindWidgets() { + MODEL_EXECUTOR.execute(() -> { + WidgetPickerConfig config = getWidgetPickerConfig(); + mModel.update(null); + + StringCache stringCache = new StringCache(); + stringCache.loadStrings(this); + + bindStringCache(stringCache); + bindWidgets(mModel.getWidgetsByPackageItemForPicker()); + // Open sheet once widgets are available, so that it doesn't interrupt the open + // animation. + openWidgetsSheet(); + config.getUiSurface(); + mWidgetPredictionsRequester = new WidgetPredictionsRequester( + getApplicationContext(), config.getUiSurface(), + mModel.getWidgetsByComponentKeyForPicker()); + mWidgetPredictionsRequester.request(mAddedWidgets, this); + }); + } + + private void bindStringCache(final StringCache stringCache) { + MAIN_EXECUTOR.execute(() -> mStringCache = stringCache); + } + + private void bindWidgets(Map> widgets) { + WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder( + getApplicationContext()); + final List allWidgets = builder.build(widgets, mNoShortcutsFilter); + + // Default list is shown if host has additionally enforced size filtering. + @Nullable Predicate defaultListFilter = + hasHostSizeFilters() ? mHostSizeAndNoShortcutsFilter : null; + + MAIN_EXECUTOR.execute(() -> { + mWidgetPickerDataProvider.setHostSpecifiedDefaultWidgetsFilter(defaultListFilter); + mWidgetPickerDataProvider.setWidgets(allWidgets); + }); + } + + private void openWidgetsSheet() { + MAIN_EXECUTOR.execute(() -> { + WidgetPickerConfig config = getWidgetPickerConfig(); + mWidgetSheet = WidgetsFullSheet.show(this, true); + mWidgetSheet.mayUpdateTitleAndDescription(config.getTitle(), + config.getDescription()); + mWidgetSheet.disableNavBarScrim(true); + mWidgetSheet.addOnCloseListener(this::finish); + }); + } + + @Override + public void onPredictionsAvailable(List recommendedWidgets) { + // Bind recommendations once picker has finished open animation. + MAIN_EXECUTOR.getHandler().postDelayed( + () -> mWidgetPickerDataProvider.setWidgetRecommendations(recommendedWidgets), + mDeviceProfile.getBottomSheetProfile().getBottomSheetOpenDuration()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (!Flags.enableWidgetPickerRefactor() || !ComposeFacade.INSTANCE.isComposeAvailable()) { + mWidgetPickerDataProvider.destroy(); + if (mWidgetPredictionsRequester != null) { + mWidgetPredictionsRequester.clear(); + } + } + } + + @Nullable + @Override + public StringCache getStringCache() { + return mStringCache; + } + + /** + * Animation callback for different predictive back animation states for the widget picker. + */ + private class BackAnimationCallback extends FlingOnBackAnimationCallback { + @Nullable + OnBackAnimationCallback mActiveOnBackAnimationCallback; + + @Override + public void onBackStartedCompat(@NonNull BackEvent backEvent) { + if (mActiveOnBackAnimationCallback != null) { + mActiveOnBackAnimationCallback.onBackCancelled(); + } + if (mWidgetSheet != null) { + mActiveOnBackAnimationCallback = mWidgetSheet; + mActiveOnBackAnimationCallback.onBackStarted(backEvent); + } + } + + @Override + public void onBackInvokedCompat() { + if (mActiveOnBackAnimationCallback == null) { + return; + } + mActiveOnBackAnimationCallback.onBackInvoked(); + mActiveOnBackAnimationCallback = null; + } + + @Override + public void onBackProgressedCompat(@NonNull BackEvent backEvent) { + if (mActiveOnBackAnimationCallback == null) { + return; + } + mActiveOnBackAnimationCallback.onBackProgressed(backEvent); + } + + @Override + public void onBackCancelledCompat() { + if (mActiveOnBackAnimationCallback == null) { + return; + } + mActiveOnBackAnimationCallback.onBackCancelled(); + mActiveOnBackAnimationCallback = null; + } + } + + private boolean hasHostSizeFilters() { + // If optional filters such as size filter are present, we display them as default widgets. + return mDesiredWidgetWidth != 0 || + mDesiredWidgetHeight != 0; + } + + private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget, + boolean applySizeFilter) { + final AppWidgetProviderInfo info = widget.widgetInfo; + if (info == null) { + return rejectWidget(widget, "shortcut"); + } + + WidgetPickerConfig config = getWidgetPickerConfig(); + if (config.getFilteredUsers().contains(widget.user)) { + return rejectWidget( + widget, + "widget user: %d is being filtered", + widget.user.getIdentifier()); + } + + if (!mWidgetCategoryInclusionFilter.matches(info.widgetCategory) + || !mWidgetCategoryExclusionFilter.matches(info.widgetCategory)) { + return rejectWidget( + widget, + "doesn't match category filter [inclusion=%d, exclusion=%d, widget=%d]", + mWidgetCategoryInclusionFilter.getCategoryMask(), + mWidgetCategoryExclusionFilter.getCategoryMask(), + info.widgetCategory); + } + + + if (applySizeFilter) { + if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) { + // Accept the widget if the desired dimensions are unspecified. + return acceptWidget(widget); + } + + final boolean isHorizontallyResizable = + (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0; + if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) { + if (info.maxResizeWidth > 0 + && info.maxResizeWidth >= info.minWidth + && info.maxResizeWidth < mDesiredWidgetWidth) { + return rejectWidget( + widget, + "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]", + info.maxResizeWidth, + mDesiredWidgetWidth); + } + + final int minWidth = min(info.minResizeWidth, info.minWidth); + if (minWidth > mDesiredWidgetWidth) { + return rejectWidget( + widget, + "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]", + info.minWidth, + info.minResizeWidth, + mDesiredWidgetWidth); + } + } + + final boolean isVerticallyResizable = + (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0; + if (mDesiredWidgetHeight > 0 && isVerticallyResizable) { + if (info.maxResizeHeight > 0 + && info.maxResizeHeight >= info.minHeight + && info.maxResizeHeight < mDesiredWidgetHeight) { + return rejectWidget( + widget, + "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]", + info.maxResizeHeight, + mDesiredWidgetHeight); + } + + final int minHeight = min(info.minResizeHeight, info.minHeight); + if (minHeight > mDesiredWidgetHeight) { + return rejectWidget( + widget, + "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]", + info.minHeight, + info.minResizeHeight, + mDesiredWidgetHeight); + } + } + + if (!isHorizontallyResizable || !isVerticallyResizable) { + return rejectWidget(widget, "not resizeable"); + } + } + + return acceptWidget(widget); + } + + private static WidgetAcceptabilityVerdict rejectWidget( + WidgetItem widget, String rejectionReason, Object... args) { + return new WidgetAcceptabilityVerdict( + false, + widget.widgetInfo != null + ? widget.widgetInfo.provider.flattenToShortString() + : widget.label, + String.format(Locale.ENGLISH, rejectionReason, args)); + } + + private static WidgetAcceptabilityVerdict acceptWidget(WidgetItem widget) { + return new WidgetAcceptabilityVerdict( + true, widget.widgetInfo.provider.flattenToShortString(), ""); + } + + private record WidgetAcceptabilityVerdict( + boolean isAcceptable, String widgetLabel, String reason) { + void maybeLogVerdict() { + // Only log a verdict if a reason is specified. + if (Log.isLoggable(TAG, Log.DEBUG) && !reason.isEmpty()) { + Log.i(TAG, String.format( + Locale.ENGLISH, + "%s: %s because %s", + widgetLabel, + isAcceptable ? "accepted" : "rejected", + reason)); + } + } + } +} diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java index 84c2ed252b..4404428f85 100644 --- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java +++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java @@ -31,12 +31,14 @@ import android.view.View; import android.view.accessibility.AccessibilityManager; import androidx.annotation.ColorInt; -import androidx.core.content.ContextCompat; +import androidx.annotation.VisibleForTesting; +import com.android.launcher3.Flags; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.FloatingHeaderRow; import com.android.launcher3.allapps.FloatingHeaderView; +import com.android.launcher3.util.Themes; /** * A view which shows a horizontal divider @@ -84,10 +86,10 @@ public class AppsDividerView extends View implements FloatingHeaderRow { getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height) }; - mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant); - - mAllAppsLabelTextColor = ContextCompat.getColor(context, - R.color.material_color_on_surface_variant); + mStrokeColor = Flags.allAppsBlur() + ? Themes.getAttrColor(context, R.attr.bottomSheetDragHandleColor) + : context.getColor(R.color.materialColorOutlineVariant); + mAllAppsLabelTextColor = context.getColor(R.color.materialColorOnSurface); mAccessibilityManager = AccessibilityManager.getInstance(context); setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context)); @@ -212,7 +214,8 @@ public class AppsDividerView extends View implements FloatingHeaderRow { private Layout getAllAppsLabelLayout() { if (mAllAppsLabelLayout == null) { mPaint.setAntiAlias(true); - mPaint.setTypeface(Typeface.create("google-sans", Typeface.NORMAL)); + mPaint.setTypeface(Typeface.create(Flags.gsfRes() ? "variable-title-small" + : "google-sans", Typeface.NORMAL)); mPaint.setTextSize( getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size)); @@ -254,4 +257,9 @@ public class AppsDividerView extends View implements FloatingHeaderRow { public View getFocusedChild() { return null; } + + @VisibleForTesting + public DividerType getDividerType() { + return mDividerType; + } } diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java index a16031d804..b6d3146ffe 100644 --- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java +++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java @@ -16,12 +16,16 @@ package com.android.launcher3.appprediction; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + import android.content.Context; import android.graphics.Canvas; +import android.os.Build; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.LinearLayout; import androidx.annotation.NonNull; @@ -31,7 +35,6 @@ import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.Flags; -import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.FloatingHeaderRow; @@ -57,7 +60,7 @@ public class PredictionRowView // Vertical padding of the icon that contributes to the expected cell height. private final int mVerticalPadding; // Extra padding that is used in the top app rows (prediction and search) that is not used in - // the regular A-Z list. This only applies to single line label. + // the regular A-Z list. private final int mTopRowExtraHeight; // Helper to drawing the focus indicator. @@ -90,6 +93,14 @@ public class PredictionRowView updateVisibility(); } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (Build.VERSION.SDK_INT >= UPSIDE_DOWN_CAKE) { + info.setContainerTitle(mActivityContext.getString(R.string.title_app_suggestions)); + } + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -132,15 +143,15 @@ public class PredictionRowView @Override public int getExpectedHeight() { DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); - int iconHeight = deviceProfile.allAppsIconSizePx; - int iconPadding = deviceProfile.allAppsIconDrawablePaddingPx; - int textHeight = Utilities.calculateTextHeight(deviceProfile.allAppsIconTextSizePx); + int iconHeight = deviceProfile.getAllAppsProfile().getIconSizePx(); + int iconPadding = deviceProfile.getAllAppsProfile().getIconDrawablePaddingPx(); + int textHeight = Utilities.calculateTextHeight( + deviceProfile.getAllAppsProfile().getIconTextSizePx()); int totalHeight = iconHeight + iconPadding + textHeight + mVerticalPadding * 2; // Prediction row height will be 4dp bigger than the regular apps in A-Z list when two line // is not enabled. Otherwise, the extra height will increase by just the textHeight. - int extraHeight = (Flags.enableTwolineToggle() && - LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext())) - ? textHeight : mTopRowExtraHeight; + int extraHeight = deviceProfile.inv.enableTwoLinesInAllApps + ? (textHeight + mTopRowExtraHeight) : mTopRowExtraHeight; totalHeight += extraHeight; return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom(); } @@ -225,7 +236,11 @@ public class PredictionRowView lp.height = ViewGroup.LayoutParams.MATCH_PARENT; } else { // Ensure the all apps icon height matches the workspace icons in portrait mode. - lp.height = mActivityContext.getDeviceProfile().allAppsCellHeightPx; + lp.height = + mActivityContext + .getDeviceProfile() + .getAllAppsProfile() + .getCellHeightPx(); } lp.width = 0; lp.weight = 1; @@ -290,6 +305,9 @@ public class PredictionRowView writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled); writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused); writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow); - writer.println(prefix + "\tmPredictedApps: " + mPredictedApps); + writer.println(prefix + "\tmPredictedApps: " + mPredictedApps.size()); + for (WorkspaceItemInfo info : mPredictedApps) { + writer.println(prefix + "\t\t" + info); + } } } diff --git a/quickstep/src/com/android/launcher3/dagger/LauncherConcurrencyModule.kt b/quickstep/src/com/android/launcher3/dagger/LauncherConcurrencyModule.kt new file mode 100644 index 0000000000..d51e014f07 --- /dev/null +++ b/quickstep/src/com/android/launcher3/dagger/LauncherConcurrencyModule.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.dagger + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.Process +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.launcher3.util.coroutines.ProductionDispatchers +import com.android.systemui.dagger.qualifiers.Background +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob + +/** Dagger Module for per-display thread handling. */ +// TODO(b/407594919) - Adapt this to use new concurrency module. +@Module +object LauncherConcurrencyModule { + // Slow BG executor can potentially affect UI if UI is waiting for an updated state from this + // thread + private const val BG_SLOW_DISPATCH_THRESHOLD = 1000L + private const val BG_SLOW_DELIVERY_THRESHOLD = 1000L + + /** Background Looper */ + @Provides + @LauncherAppSingleton + @Background + fun provideBgLooper(): Looper { + val thread = HandlerThread("LauncherBg", Process.THREAD_PRIORITY_BACKGROUND) + thread.start() + thread + .getLooper() + .setSlowLogThresholdMs(BG_SLOW_DISPATCH_THRESHOLD, BG_SLOW_DELIVERY_THRESHOLD) + return thread.getLooper() + } + + /** + * Background Handler. + * + * Prefer the Background Executor when possible. + */ + @Provides + @LauncherAppSingleton + @Background + fun provideBgHandler(@Background bgLooper: Looper): Handler = Handler(bgLooper) + + /** CoroutineDispatcher provider. */ + @Provides fun provideCoroutineDispatcherProvider(): DispatcherProvider = ProductionDispatchers + + /** Background CoroutineScope provider. */ + @Provides + @LauncherAppSingleton + @Background + fun provideBgCoroutineScope(dispatcherProvider: DispatcherProvider) = + CoroutineScope( + SupervisorJob() + dispatcherProvider.ioBackground + CoroutineName("LauncherBg") + ) +} diff --git a/quickstep/src/com/android/launcher3/dagger/Modules.kt b/quickstep/src/com/android/launcher3/dagger/Modules.kt new file mode 100644 index 0000000000..479c3eb82a --- /dev/null +++ b/quickstep/src/com/android/launcher3/dagger/Modules.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.dagger + +import com.android.launcher3.backuprestore.LauncherRestoreEventLogger +import com.android.launcher3.icons.LauncherIconProvider +import com.android.launcher3.icons.LauncherIconProviderImpl +import com.android.launcher3.logging.StatsLogManager.StatsLogManagerFactory +import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepWidgetHolderFactory +import com.android.launcher3.uioverrides.SystemApiWrapper +import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl +import com.android.launcher3.util.ApiWrapper +import com.android.launcher3.util.InstantAppResolver +import com.android.launcher3.util.PluginManagerWrapper +import com.android.launcher3.util.window.RefreshRateTracker +import com.android.launcher3.util.window.WindowManagerProxy +import com.android.launcher3.widget.LauncherWidgetHolder.WidgetHolderFactory +import com.android.quickstep.InstantAppResolverImpl +import com.android.quickstep.LauncherRestoreEventLoggerImpl +import com.android.quickstep.logging.StatsLogCompatManager.StatsLogCompatManagerFactory +import com.android.quickstep.util.ChoreographerFrameRateTracker +import com.android.quickstep.util.GestureExclusionManager +import com.android.systemui.shared.system.ActivityManagerWrapper +import dagger.Binds +import dagger.Module +import dagger.Provides + +import app.lawnchair.factory.LawnchairWidgetHolder +import app.lawnchair.util.LawnchairWindowManagerProxy + +private object Modules {} + +@Module +abstract class WindowManagerProxyModule { + @Binds abstract fun bindWindowManagerProxy(proxy: LawnchairWindowManagerProxy): WindowManagerProxy +} + +@Module +abstract class ApiWrapperModule { + @Binds + abstract fun bindStatsLogManagerFactory( + impl: StatsLogCompatManagerFactory + ): StatsLogManagerFactory + + @Binds abstract fun bindApiWrapper(systemApiWrapper: SystemApiWrapper): ApiWrapper + + @Binds + abstract fun bindIconProvider(iconProviderImpl: LauncherIconProviderImpl): LauncherIconProvider + + @Binds abstract fun bindInstantAppResolver(impl: InstantAppResolverImpl): InstantAppResolver + + @Binds + abstract fun bindRestoreEventLogger( + impl: LauncherRestoreEventLoggerImpl + ): LauncherRestoreEventLogger +} + +@Module +abstract class WidgetModule { + + @Binds + abstract fun bindWidgetHolderFactory(factor: LawnchairWidgetHolder.Factory): WidgetHolderFactory +} + +@Module +abstract class PluginManagerWrapperModule { + @Binds + abstract fun bindPluginManagerWrapper(impl: PluginManagerWrapperImpl): PluginManagerWrapper +} + +@Module +object StaticObjectModule { + + @Provides + @JvmStatic + fun provideGestureExclusionManager(): GestureExclusionManager = GestureExclusionManager.INSTANCE + + @Provides + @JvmStatic + fun provideRefreshRateTracker(): RefreshRateTracker = ChoreographerFrameRateTracker + + @Provides + @JvmStatic + fun provideActivityManagerWrapper(): ActivityManagerWrapper = + ActivityManagerWrapper.getInstance() +} diff --git a/quickstep/src/com/android/launcher3/dagger/PerDisplayModule.kt b/quickstep/src/com/android/launcher3/dagger/PerDisplayModule.kt new file mode 100644 index 0000000000..256d9272e7 --- /dev/null +++ b/quickstep/src/com/android/launcher3/dagger/PerDisplayModule.kt @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.dagger + +import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Handler +import android.util.Log +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY +import android.view.WindowManagerGlobal +import com.android.app.displaylib.DefaultDisplayOnlyInstanceRepositoryImpl +import com.android.app.displaylib.DisplayLibBackground +import com.android.app.displaylib.DisplayLibComponent +import com.android.app.displaylib.DisplayRepository +import com.android.app.displaylib.DisplaysWithDecorationsRepository +import com.android.app.displaylib.DisplaysWithDecorationsRepositoryCompat +import com.android.app.displaylib.PerDisplayInstanceRepositoryImpl +import com.android.app.displaylib.PerDisplayRepository +import com.android.app.displaylib.SingleInstanceRepositoryImpl +import com.android.app.displaylib.createDisplayLibComponent +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.FallbackWindowInterface +import com.android.quickstep.RecentsAnimationDeviceState +import com.android.quickstep.RotationTouchHelper +import com.android.quickstep.TaskAnimationManager +import com.android.quickstep.fallback.window.RecentsWindowFlags.enableOverviewOnConnectedDisplays +import com.android.quickstep.fallback.window.RecentsWindowManager +import com.android.quickstep.fallback.window.RecentsWindowManagerInstanceProvider +import com.android.systemui.dagger.qualifiers.Background +import dagger.Binds +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineScope + +@Module(includes = [BasePerDisplayModule::class, PerDisplayRepositoriesModule::class]) +interface PerDisplayModule + +@Module(includes = [DisplayLibModule::class]) +interface BasePerDisplayModule { + @Binds + @DisplayLibBackground + abstract fun bindDisplayLibBackground(@Background bgScope: CoroutineScope): CoroutineScope +} + +@Module +object PerDisplayRepositoriesModule { + @Provides + @LauncherAppSingleton + fun provideRecentsAnimationDeviceStateRepo( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory, + instanceFactory: RecentsAnimationDeviceState.Factory, + rotationTouchHelperRepository: PerDisplayRepository, + ): PerDisplayRepository { + return if (enableOverviewOnConnectedDisplays()) { + repositoryFactory.create( + "RecentsAnimationDeviceStateRepo", + { displayId -> + rotationTouchHelperRepository[displayId]?.let { + instanceFactory.create(displayId, it) + } + }, + ) + } else { + SingleInstanceRepositoryImpl( + "RecentsAnimationDeviceStateRepo", + rotationTouchHelperRepository[DEFAULT_DISPLAY]?.let { + instanceFactory.create(DEFAULT_DISPLAY, it) + }!!, // Assert the default display is always available. + ) + } + } + + @Provides + @LauncherAppSingleton + fun provideTaskAnimationManagerRepo( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory, + instanceFactory: TaskAnimationManager.Factory, + ): PerDisplayRepository { + return if (enableOverviewOnConnectedDisplays()) { + repositoryFactory.create("TaskAnimationManagerRepo", instanceFactory::create) + } else { + SingleInstanceRepositoryImpl( + "TaskAnimationManager", + instanceFactory.create(DEFAULT_DISPLAY), + ) + } + } + + @Provides + @LauncherAppSingleton + fun provideRotationTouchHandlerRepo( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory, + instanceFactory: RotationTouchHelper.Factory, + @WindowContext windowContextRepository: PerDisplayRepository, + ): PerDisplayRepository { + return if (enableOverviewOnConnectedDisplays()) { + repositoryFactory.create( + "RotationTouchHelperRepo", + { displayId -> + windowContextRepository[displayId]?.let { instanceFactory.create(it) } + }, + ) + } else { + SingleInstanceRepositoryImpl( + "RotationTouchHelperRepo", + instanceFactory.create(windowContextRepository[DEFAULT_DISPLAY]), + ) + } + } + + @Provides + @LauncherAppSingleton + fun provideFallbackWindowInterfaceRepo( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory + ): PerDisplayRepository { + return if (enableOverviewOnConnectedDisplays()) { + repositoryFactory.create( + "FallbackWindowInterfaceRepo", + { _ -> FallbackWindowInterface() }, + ) + } else { + SingleInstanceRepositoryImpl("FallbackWindowInterfaceRepo", FallbackWindowInterface()) + } + } + + @Provides + @LauncherAppSingleton + fun provideRecentsWindowManagerRepo( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory, + instanceProvider: RecentsWindowManagerInstanceProvider, + ): PerDisplayRepository { + return if (enableOverviewOnConnectedDisplays()) { + repositoryFactory.create("RecentsWindowManagerRepo", instanceProvider) + } else { + DefaultDisplayOnlyInstanceRepositoryImpl("RecentsWindowManagerRepo", instanceProvider) + } + } + + @Provides + @LauncherAppSingleton + @DisplayContext + fun provideDisplayContext( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory, + displayRepository: DisplayRepository, + @ApplicationContext context: Context, + ): PerDisplayRepository { + return if (enableOverviewOnConnectedDisplays()) { + repositoryFactory.create( + "DisplayContextRepo", + { displayId -> + displayRepository.getDisplay(displayId)?.let { + context.createDisplayContext(it) + } + }, + ) + } else { + SingleInstanceRepositoryImpl( + "DisplayContextRepo", + context.createDisplayContext(displayRepository.getDisplay(DEFAULT_DISPLAY)!!), + ) + } + } + + @Provides + @LauncherAppSingleton + @WindowContext + fun provideWindowContext( + repositoryFactory: PerDisplayInstanceRepositoryImpl.Factory, + displayRepository: DisplayRepository, + @ApplicationContext context: Context, + ): PerDisplayRepository { + return if (enableOverviewOnConnectedDisplays()) { + repositoryFactory.create( + "DisplayContextRepo", + { displayId -> + displayRepository.getDisplay(displayId)?.let { + context.createWindowContext( + it, + TYPE_APPLICATION_OVERLAY, + /* options=*/ null, + ) + } + }, + ) + } else { + SingleInstanceRepositoryImpl( + "DisplayContextRepo", + context.createWindowContext( + displayRepository.getDisplay(DEFAULT_DISPLAY)!!, + TYPE_APPLICATION_OVERLAY, + /* options=*/ null, + ), + ) + } + } +} + +/** + * Module to bind the DisplayRepository from displaylib to the LauncherAppSingleton dagger graph. + */ +@Module +object DisplayLibModule { + @Provides + @LauncherAppSingleton + fun displayLibComponent( + @ApplicationContext context: Context, + @Background bgHandler: Handler, + @Background bgApplicationScope: CoroutineScope, + coroutineDispatcherProvider: DispatcherProvider, + ): DisplayLibComponent { + val displayManager = context.getSystemService(DisplayManager::class.java) + val windowManager = checkNotNull(WindowManagerGlobal.getWindowManagerService()) + return createDisplayLibComponent( + displayManager, + windowManager, + bgHandler, + bgApplicationScope, + coroutineDispatcherProvider.ioBackground, + ) + } + + @Provides + @LauncherAppSingleton + fun providesDisplayRepositoryFromLib( + displayLibComponent: DisplayLibComponent + ): DisplayRepository { + return displayLibComponent.displayRepository + } + + @Provides + @LauncherAppSingleton + fun providesDisplaysWithDecorationsRepository( + displayLibComponent: DisplayLibComponent + ): DisplaysWithDecorationsRepository { + return displayLibComponent.displaysWithDecorationsRepository + } + + @Provides + @LauncherAppSingleton + fun providesDisplaysWithDecorationsRepositoryCompat( + displayLibComponent: DisplayLibComponent + ): DisplaysWithDecorationsRepositoryCompat { + return displayLibComponent.displaysWithDecorationsRepositoryCompat + } + + @Provides + fun dumpRegistrationLambda(): PerDisplayRepository.InitCallback = + PerDisplayRepository.InitCallback { debugName, _ -> + Log.d("PerDisplayInitCallback", debugName) + } +} diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt new file mode 100644 index 0000000000..a9e5145530 --- /dev/null +++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.desktop + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Rect +import android.util.Log +import android.view.Choreographer +import android.view.SurfaceControl.Transaction +import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.DesktopModeFlags +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import androidx.core.animation.addListener +import androidx.core.util.Supplier +import com.android.app.animation.Interpolators +import com.android.internal.jank.Cuj +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.policy.ScreenDecorationsUtils +import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType +import com.android.launcher3.desktop.DesktopAppLaunchTransition.Companion.LAUNCH_CHANGE_MODES +import com.android.wm.shell.shared.animation.MinimizeAnimator +import com.android.wm.shell.shared.animation.WindowAnimator + +/** + * Helper class responsible for creating and managing animators for desktop app launch and related + * transitions. + * + *

This class handles the complex logic of creating various animators, including launch, + * minimize, and trampoline close animations, based on the provided transition information and + * launch type. It also utilizes {@link InteractionJankMonitor} to monitor animation jank. + * + * @param context The application context. + * @param launchType The type of app launch, containing animation parameters. + * @param cujType The CUJ (Critical User Journey) type for jank monitoring. + */ +class DesktopAppLaunchAnimatorHelper( + private val context: Context, + private val launchType: AppLaunchType, + @Cuj.CujType private val cujType: Int, + private val transactionSupplier: Supplier, +) { + + private val interactionJankMonitor = InteractionJankMonitor.getInstance() + + fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List { + val launchChange = getLaunchChange(info) + if (launchChange == null) { + val tasksInfo = + info.changes.joinToString(", ") { change -> + "${change.taskInfo?.taskId}:${change.taskInfo?.isFreeform}" + } + Log.e(TAG, "No launch change found: Transition info=$info, tasks state=$tasksInfo") + return emptyList() + } + + val transaction = transactionSupplier.get() + + val minimizeChange = getMinimizeChange(info) + val trampolineCloseChange = getTrampolineCloseChange(info) + + val launchAnimator = + createLaunchAnimator( + launchChange, + transaction, + finishCallback, + isTrampoline = trampolineCloseChange != null, + ) + val animatorsList = mutableListOf(launchAnimator) + if (minimizeChange != null) { + val minimizeAnimator = + createMinimizeAnimator(minimizeChange, transaction, finishCallback) + animatorsList.add(minimizeAnimator) + } + if (trampolineCloseChange != null) { + val trampolineCloseAnimator = + createTrampolineCloseAnimator(trampolineCloseChange, transaction, finishCallback) + animatorsList.add(trampolineCloseAnimator) + } + return animatorsList + } + + private fun getLaunchChange(info: TransitionInfo): Change? = + info.changes.firstOrNull { change -> + change.mode in LAUNCH_CHANGE_MODES && change.taskInfo?.isFreeform == true + } + + private fun getMinimizeChange(info: TransitionInfo): Change? = + info.changes.firstOrNull { change -> + change.mode == TRANSIT_TO_BACK && change.taskInfo?.isFreeform == true + } + + private fun getTrampolineCloseChange(info: TransitionInfo): Change? { + if ( + info.changes.size < 2 || + !DesktopModeFlags.ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX.isTrue + ) { + return null + } + val openChange = + info.changes.firstOrNull { change -> + change.mode == TRANSIT_OPEN && change.taskInfo?.isFreeform == true + } + val closeChange = + info.changes.firstOrNull { change -> + change.mode == TRANSIT_CLOSE && change.taskInfo?.isFreeform == true + } + val openPackage = openChange?.taskInfo?.baseIntent?.component?.packageName + val closePackage = closeChange?.taskInfo?.baseIntent?.component?.packageName + return if (openPackage != null && closePackage != null && openPackage == closePackage) { + closeChange + } else { + null + } + } + + private fun createLaunchAnimator( + change: Change, + transaction: Transaction, + onAnimFinish: (Animator) -> Unit, + isTrampoline: Boolean, + ): Animator { + val boundsAnimator = + WindowAnimator.createBoundsAnimator( + context.resources.displayMetrics, + launchType.boundsAnimationParams, + change, + transaction, + ) + val alphaAnimator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = launchType.alphaDurationMs + interpolator = Interpolators.LINEAR + addUpdateListener { animation -> + transaction + .setAlpha(change.leash, animation.animatedValue as Float) + .setFrameTimeline(Choreographer.getInstance().vsyncId) + .apply() + } + } + val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) } + transaction.setCrop(change.leash, clipRect) + transaction.setCornerRadius( + change.leash, + ScreenDecorationsUtils.getWindowCornerRadius(context), + ) + return AnimatorSet().apply { + interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType) + if (isTrampoline) { + play(alphaAnimator) + } else { + playTogether(boundsAnimator, alphaAnimator) + } + addListener( + onEnd = { animation -> + onAnimFinish(animation) + interactionJankMonitor.end(cujType) + } + ) + } + } + + private fun createMinimizeAnimator( + change: Change, + transaction: Transaction, + onAnimFinish: (Animator) -> Unit, + ): Animator { + return MinimizeAnimator.create( + context, + change, + transaction, + onAnimFinish, + interactionJankMonitor, + context.mainThreadHandler, + ) + } + + private fun createTrampolineCloseAnimator( + change: Change, + transaction: Transaction, + onAnimFinish: (Animator) -> Unit, + ): Animator { + return ValueAnimator.ofFloat(1f, 0f).apply { + duration = 100L + interpolator = Interpolators.LINEAR + addUpdateListener { animation -> + transaction.setAlpha(change.leash, animation.animatedValue as Float).apply() + } + addListener( + onEnd = { animation -> + onAnimFinish(animation) + } + ) + } + } + + private companion object { + const val TAG = "DesktopAppLaunchAnimatorHelper" + } +} diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt new file mode 100644 index 0000000000..281d84677a --- /dev/null +++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt @@ -0,0 +1,117 @@ +/* + * 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.launcher3.desktop + +import android.animation.Animator +import android.content.Context +import android.os.IBinder +import android.util.Log +import android.view.SurfaceControl.Transaction +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.IRemoteTransitionFinishedCallback +import android.window.RemoteTransitionStub +import android.window.TransitionInfo +import androidx.core.util.Supplier +import com.android.app.animation.Interpolators +import com.android.internal.jank.Cuj +import com.android.quickstep.RemoteRunnable +import com.android.wm.shell.shared.animation.WindowAnimator +import java.util.concurrent.Executor + +/** + * [android.window.RemoteTransition] for Desktop app launches. + * + * This transition supports minimize-changes, i.e. in a launch-transition, if a window is moved back + * ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to + * that window. + */ +class DesktopAppLaunchTransition +@JvmOverloads +constructor( + context: Context, + private val launchType: AppLaunchType, + @Cuj.CujType private val cujType: Int, + private val mainExecutor: Executor, + transactionSupplier: Supplier = Supplier { Transaction() }, +) : RemoteTransitionStub() { + + private val animatorHelper: DesktopAppLaunchAnimatorHelper = + DesktopAppLaunchAnimatorHelper(context, launchType, cujType, transactionSupplier) + + enum class AppLaunchType( + val boundsAnimationParams: WindowAnimator.BoundsAnimationParams, + val alphaDurationMs: Long, + ) { + LAUNCH(launchBoundsAnimationDef, /* alphaDurationMs= */ 200L), + UNMINIMIZE(unminimizeBoundsAnimationDef, /* alphaDurationMs= */ 100L), + } + + override fun startAnimation( + token: IBinder, + info: TransitionInfo, + transaction: Transaction, + transitionFinishedCallback: IRemoteTransitionFinishedCallback, + ) { + Log.v(TAG, "startAnimation: launchType=$launchType, cujType=$cujType") + val safeTransitionFinishedCallback = RemoteRunnable { + transitionFinishedCallback.onTransitionFinished(/* wct= */ null, /* sct= */ null) + } + mainExecutor.execute { + runAnimators(info, safeTransitionFinishedCallback) + transaction.apply() + } + } + + private fun runAnimators(info: TransitionInfo, finishedCallback: RemoteRunnable) { + val animators = mutableListOf() + val animatorFinishedCallback: (Animator) -> Unit = { animator -> + animators -= animator + if (animators.isEmpty()) { + RemoteRunnable.executeSafely(finishedCallback) + } + } + animators += animatorHelper.createAnimators(info, animatorFinishedCallback) + if (animators.isEmpty()) { + RemoteRunnable.executeSafely(finishedCallback) + return + } + animators.forEach { it.start() } + } + + companion object { + const val TAG = "DesktopAppLaunchTransition" + /** Change modes that represent a task becoming visible / launching in Desktop mode. */ + val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT) + + private val launchBoundsAnimationDef = + WindowAnimator.BoundsAnimationParams( + durationMs = 600, + startOffsetYDp = 36f, + startScale = 0.95f, + interpolator = Interpolators.STANDARD_DECELERATE, + ) + + private val unminimizeBoundsAnimationDef = + WindowAnimator.BoundsAnimationParams( + durationMs = 300, + startOffsetYDp = 12f, + startScale = 0.97f, + interpolator = Interpolators.STANDARD_DECELERATE, + ) + } +} diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt new file mode 100644 index 0000000000..1434168687 --- /dev/null +++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt @@ -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. + */ + +package com.android.launcher3.desktop + +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.Context +import android.window.DesktopExperienceFlags +import android.window.DesktopModeFlags +import android.window.RemoteTransition +import android.window.TransitionFilter +import android.window.TransitionFilter.CONTAINER_ORDER_TOP +import com.android.internal.jank.Cuj +import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.quickstep.SystemUiProxy +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus + +/** Manages transitions related to app launches in Desktop Mode. */ +class DesktopAppLaunchTransitionManager( + private val context: Context, + private val systemUiProxy: SystemUiProxy, +) { + private var remoteWindowLimitUnminimizeTransition: RemoteTransition? = null + + /** + * Register a [RemoteTransition] supporting Desktop app launches, and window limit + * minimizations. + */ + fun registerTransitions() { + if (!shouldRegisterTransitions()) { + return + } + remoteWindowLimitUnminimizeTransition = + RemoteTransition( + DesktopAppLaunchTransition( + context, + AppLaunchType.UNMINIMIZE, + Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT, + MAIN_EXECUTOR, + ), + "DesktopWindowLimitUnminimize", + ) + systemUiProxy.registerRemoteTransition( + remoteWindowLimitUnminimizeTransition, + buildAppLaunchFilter(), + ) + } + + /** + * Unregister the [RemoteTransition] supporting Desktop app launches and window limit + * minimizations. + */ + fun unregisterTransitions() { + if (!shouldRegisterTransitions()) { + return + } + systemUiProxy.unregisterRemoteTransition(remoteWindowLimitUnminimizeTransition) + remoteWindowLimitUnminimizeTransition = null + } + + private fun shouldRegisterTransitions(): Boolean = + DesktopModeStatus.canEnterDesktopMode(context) && + DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue + + companion object { + private fun buildAppLaunchFilter(): TransitionFilter { + val openRequirement = + TransitionFilter.Requirement().apply { + mActivityType = ACTIVITY_TYPE_STANDARD + mWindowingMode = WINDOWING_MODE_FREEFORM + mModes = DesktopAppLaunchTransition.LAUNCH_CHANGE_MODES + mMustBeTask = true + if (!DesktopExperienceFlags.ENABLE_DESKTOP_APP_LAUNCH_BUGFIX.isTrue) { + mOrder = CONTAINER_ORDER_TOP + } + } + return TransitionFilter().apply { + mTypeSet = DesktopAppLaunchTransition.LAUNCH_CHANGE_MODES + mRequirements = arrayOf(openRequirement) + } + } + } +} diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt index 9178062c2d..6d9b087647 100644 --- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt +++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt @@ -20,6 +20,7 @@ import android.os.IBinder import android.os.RemoteException import android.util.Log import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IRemoteTransitionFinishedCallback import android.window.RemoteTransition import android.window.RemoteTransitionStub @@ -29,8 +30,12 @@ import com.android.launcher3.statemanager.StateManager import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.quickstep.SystemUiProxy import com.android.quickstep.TaskViewUtils +import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled import com.android.quickstep.views.DesktopTaskView -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskView +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import java.util.function.Consumer /** Manage recents related operations with desktop tasks */ @@ -38,42 +43,71 @@ class DesktopRecentsTransitionController( private val stateManager: StateManager<*, *>, private val systemUiProxy: SystemUiProxy, private val appThread: IApplicationThread, - private val depthController: DepthController? + private val depthController: DepthController?, ) { - /** Launch desktop tasks from recents view */ + /** + * Launch desktop tasks from recents view and activate the new freeform task with id + * [taskIdToReorderToFront] if it's provided and already on the given desk. + */ fun launchDesktopFromRecents( desktopTaskView: DesktopTaskView, - callback: Consumer? = null + animated: Boolean, + taskIdToReorderToFront: Int? = null, + callback: Consumer? = null, ) { val animRunner = RemoteDesktopLaunchTransitionRunner( desktopTaskView, + animated, stateManager, depthController, - callback + callback, ) val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop") - systemUiProxy.showDesktopApps(desktopTaskView.display.displayId, transition) + if (areMultiDesksFlagsEnabled()) { + systemUiProxy.activateDesk(desktopTaskView.deskId, transition, taskIdToReorderToFront) + } else { + systemUiProxy.showDesktopApps( + desktopTaskView.displayId, + transition, + taskIdToReorderToFront, + ) + } } /** Launch desktop tasks from recents view */ - fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) { - systemUiProxy.moveToDesktop(taskId, transitionSource) + fun moveToDesktop( + taskContainer: TaskContainer, + transitionSource: DesktopModeTransitionSource, + successCallback: Runnable, + ) { + systemUiProxy.moveToDesktop( + taskContainer.task.key.id, + transitionSource, + /* transition = */ null, + successCallback, + ) + } + + /** Move task to external display from recents view */ + fun moveToExternalDisplay(taskId: Int) { + systemUiProxy.moveToExternalDisplay(taskId) } private class RemoteDesktopLaunchTransitionRunner( - private val desktopTaskView: DesktopTaskView, + private val taskView: TaskView, + private val animated: Boolean, private val stateManager: StateManager<*, *>, private val depthController: DepthController?, - private val successCallback: Consumer? + private val successCallback: Consumer?, ) : RemoteTransitionStub() { override fun startAnimation( token: IBinder, info: TransitionInfo, t: SurfaceControl.Transaction, - finishCallback: IRemoteTransitionFinishedCallback + finishCallback: IRemoteTransitionFinishedCallback, ) { val errorHandlingFinishCallback = Runnable { try { @@ -83,16 +117,44 @@ class DesktopRecentsTransitionController( } } + if (Flags.enableDesktopWindowingPersistence()) { + handleAnimationAfterReboot(info) + } MAIN_EXECUTOR.execute { - TaskViewUtils.composeRecentsDesktopLaunchAnimator( - desktopTaskView, - stateManager, - depthController, - info, - t + val animator = + TaskViewUtils.composeRecentsDesktopLaunchAnimator( + taskView, + stateManager, + depthController, + info, + t, + ) { + errorHandlingFinishCallback.run() + successCallback?.accept(true) + } + if (!animated) { + animator.setDuration(0) + } + animator.start() + } + } + + /** + * Upon reboot the start bounds of a task is set to fullscreen with the recents transition. + * Check this case and set the start bounds to the end bounds so that the window doesn't + * jump from start bounds to end bounds during the animation. Tasks in desktop cannot + * normally have top bound as 0 due to status bar so this is a good indicator to identify + * reboot case. + */ + private fun handleAnimationAfterReboot(info: TransitionInfo) { + info.changes.forEach { change -> + if ( + change.mode == TRANSIT_TO_FRONT && + change.taskInfo?.isFreeform == true && + change.startAbsBounds.top == 0 && + change.startAbsBounds.left == 0 ) { - errorHandlingFinishCallback.run() - successCallback?.accept(true) + change.setStartAbsBounds(change.endAbsBounds) } } } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java index b77c43fc2f..7c53360ea4 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java @@ -33,7 +33,6 @@ import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.util.IntArray; import com.android.launcher3.views.ArrowTipView; import com.android.launcher3.views.Snackbar; @@ -57,7 +56,6 @@ public class HotseatEduController { private HotseatEduDialog mActiveDialog; private ArrayList mNewItems = new ArrayList<>(); - private IntArray mNewScreens = null; HotseatEduController(Launcher launcher) { mLauncher = launcher; @@ -96,7 +94,6 @@ public class HotseatEduController { } if (pageId == -1) { pageId = mLauncher.getModel().getModelDbController().getNewScreenId(); - mNewScreens = IntArray.wrap(pageId); } boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout(); int hotseatItemsNum = mLauncher.getDeviceProfile().numShownHotseatIcons; @@ -117,18 +114,7 @@ public class HotseatEduController { void moveHotseatItems() { mHotseat.removeAllViewsInLayout(); if (!mNewItems.isEmpty()) { - int lastPage = mNewItems.get(mNewItems.size() - 1).screenId; - ArrayList animated = new ArrayList<>(); - ArrayList nonAnimated = new ArrayList<>(); - - for (ItemInfo info : mNewItems) { - if (info.screenId == lastPage) { - animated.add(info); - } else { - nonAnimated.add(info); - } - } - mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated); + mLauncher.bindItemsAdded(mNewItems); } } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java index db225be4a9..891397fe84 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java @@ -38,7 +38,6 @@ import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.uioverrides.PredictedAppIcon; import com.android.launcher3.views.AbstractSlideInView; import java.util.List; @@ -89,11 +88,11 @@ public class HotseatEduDialog extends AbstractSlideInView implements I mSampleHotseat = findViewById(R.id.sample_prediction); Context context = getContext(); - DeviceProfile grid = mActivityContext.getDeviceProfile(); - Rect padding = grid.getHotseatLayoutPadding(context); + DeviceProfile dp = mActivityContext.getDeviceProfile(); + Rect padding = dp.getHotseatLayoutPadding(context); - mSampleHotseat.getLayoutParams().height = grid.cellHeightPx; - mSampleHotseat.setGridSize(grid.numShownHotseatIcons, 1); + mSampleHotseat.getLayoutParams().height = dp.cellHeightPx; + mSampleHotseat.setGridSize(dp.numShownHotseatIcons, 1); mSampleHotseat.setPadding(padding.left, 0, padding.right, 0); Button turnOnBtn = findViewById(R.id.turn_predictions_on); @@ -103,7 +102,8 @@ public class HotseatEduDialog extends AbstractSlideInView implements I mDismissBtn.setOnClickListener(this::onDismiss); LinearLayout buttonContainer = findViewById(R.id.button_container); - int adjustedMarginEnd = grid.hotseatBarEndOffset - buttonContainer.getPaddingEnd(); + int adjustedMarginEnd = + dp.getHotseatProfile().getBarEndOffset() - buttonContainer.getPaddingEnd(); if (InvariantDeviceProfile.INSTANCE.get(context) .getDeviceProfile(context).isTaskbarPresent && adjustedMarginEnd > 0) { ((LinearLayout.LayoutParams) buttonContainer.getLayoutParams()).setMarginEnd( @@ -184,10 +184,9 @@ public class HotseatEduDialog extends AbstractSlideInView implements I private void populatePreview(List predictions) { for (int i = 0; i < mActivityContext.getDeviceProfile().numShownHotseatIcons; i++) { WorkspaceItemInfo info = predictions.get(i); - PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info); + View icon = mActivityContext.getItemInflater().inflateItem(info, mSampleHotseat); icon.setEnabled(false); icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - icon.verifyHighRes(); CellLayoutLayoutParams lp = new CellLayoutLayoutParams(i, 0, 1, 1); mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true); } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index de974ecae1..af68545cc8 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -29,7 +29,6 @@ import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.ComponentName; -import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.View; import android.view.ViewGroup; @@ -47,14 +46,15 @@ import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.graphics.DragPreviewProvider; import com.android.launcher3.logger.LauncherAtom.ContainerInfo; import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer; import com.android.launcher3.logging.InstanceId; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.popup.SystemShortcut; @@ -81,11 +81,9 @@ public class HotseatPredictionController implements DragController.DragListener, SystemShortcut.Factory, DeviceProfile.OnDeviceProfileChangeListener, DragSource, ViewGroup.OnHierarchyChangeListener { - private static final String TAG = "HotseatPredictionController"; - private static final int FLAG_UPDATE_PAUSED = 1 << 0; - private static final int FLAG_DRAG_IN_PROGRESS = 1 << 1; - private static final int FLAG_FILL_IN_PROGRESS = 1 << 2; - private static final int FLAG_REMOVING_PREDICTED_ICON = 1 << 3; + private static final int FLAG_DRAG_IN_PROGRESS = 1 << 0; + private static final int FLAG_FILL_IN_PROGRESS = 1 << 1; + private static final int FLAG_REMOVING_PREDICTED_ICON = 1 << 2; private int mHotSeatItemsCount; @@ -229,12 +227,10 @@ public class HotseatPredictionController implements DragController.DragListener, (WorkspaceItemInfo) mPredictedItems.get(predictionIndex++); if (isPredictedIcon(child) && child.isEnabled()) { PredictedAppIcon icon = (PredictedAppIcon) child; - boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem); - icon.applyFromWorkspaceItem(predictedItem, animateIconChange, numViewsAnimated); - if (animateIconChange) { + if (icon.applyFromWorkspaceItemWithAnimation(predictedItem, numViewsAnimated)) { numViewsAnimated++; } - icon.finishBinding(mPredictionLongClickListener); + finishBinding(icon); } else { newItems.add(predictedItem); } @@ -248,9 +244,9 @@ public class HotseatPredictionController implements DragController.DragListener, private void bindItems(List itemsToAdd, boolean animate) { AnimatorSet animationSet = new AnimatorSet(); for (WorkspaceItemInfo item : itemsToAdd) { - PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item); + View icon = mLauncher.getItemInflater().inflateItem(item, mHotseat); mLauncher.getWorkspace().addInScreenFromBind(icon, item); - icon.finishBinding(mPredictionLongClickListener); + finishBinding(icon); if (animate) { animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1)); } @@ -264,6 +260,11 @@ public class HotseatPredictionController implements DragController.DragListener, } } + private void finishBinding(View view) { + view.setOnLongClickListener(mPredictionLongClickListener); + ((CellLayoutLayoutParams) view.getLayoutParams()).canReorder = false; + } + private void removeOutlineDrawings() { if (mOutlineDrawings.isEmpty()) return; for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) { @@ -281,33 +282,11 @@ public class HotseatPredictionController implements DragController.DragListener, mLauncher.removeOnDeviceProfileChangeListener(this); } - /** - * start and pauses predicted apps update on the hotseat - */ - public void setPauseUIUpdate(boolean paused) { - mPauseFlags = paused - ? (mPauseFlags | FLAG_UPDATE_PAUSED) - : (mPauseFlags & ~FLAG_UPDATE_PAUSED); - if (!paused) { - fillGapsWithPrediction(); - } - } - - /** - * Ensures that if the flag FLAG_UPDATE_PAUSED is active we set it to false. - */ - public void verifyUIUpdateNotPaused() { - if ((mPauseFlags & FLAG_UPDATE_PAUSED) != 0) { - setPauseUIUpdate(false); - Log.e(TAG, "FLAG_UPDATE_PAUSED should not be set to true (see b/339700174)"); - } - } - /** * Sets or updates the predicted items */ - public void setPredictedItems(FixedContainerItems items) { - mPredictedItems = new ArrayList(items.items); + public void setPredictedItems(PredictedContainerInfo items) { + mPredictedItems = items.getContents(); if (mPredictedItems.isEmpty()) { HotseatRestoreHelper.restoreBackup(mLauncher); } @@ -505,10 +484,18 @@ public class HotseatPredictionController implements DragController.DragListener, private class PinPrediction extends SystemShortcut { private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo, View originalView) { - super(R.drawable.ic_pin, R.string.pin_prediction, target, + super(getDrawableId(), R.string.pin_prediction, target, itemInfo, originalView); } + public static int getDrawableId() { + if (Flags.enableLauncherVisualRefresh()) { + return R.drawable.keep_24px; + } else { + return R.drawable.ic_pin; + } + } + @Override public void onClick(View view) { dismissTaskMenuView(); @@ -524,7 +511,6 @@ public class HotseatPredictionController implements DragController.DragListener, private static String getStateString(int flags) { StringJoiner str = new StringJoiner("|"); - appendFlag(str, flags, FLAG_UPDATE_PAUSED, "FLAG_UPDATE_PAUSED"); appendFlag(str, flags, FLAG_DRAG_IN_PROGRESS, "FLAG_DRAG_IN_PROGRESS"); appendFlag(str, flags, FLAG_FILL_IN_PROGRESS, "FLAG_FILL_IN_PROGRESS"); appendFlag(str, flags, FLAG_REMOVING_PREDICTED_ICON, @@ -536,6 +522,9 @@ public class HotseatPredictionController implements DragController.DragListener, writer.println(prefix + "HotseatPredictionController"); writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags)); writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount); - writer.println(prefix + "\tmPredictedItems: " + mPredictedItems); + writer.println(prefix + "\tmPredictedItems: " + mPredictedItems.size()); + for (ItemInfo info : mPredictedItems) { + writer.println(prefix + "\t\t" + info); + } } } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java deleted file mode 100644 index 56945ba0a6..0000000000 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.hybridhotseat; - -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; -import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo; -import static com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction; -import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation; - -import android.app.prediction.AppTarget; -import android.app.prediction.AppTargetEvent; -import android.content.Context; -import android.os.Bundle; - -import com.android.launcher3.model.BgDataModel; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; -import com.android.launcher3.model.data.ItemInfo; - -import java.util.ArrayList; - -/** - * Model helper for app predictions in workspace - */ -public class HotseatPredictionModel { - private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events"; - private static final String BUNDLE_KEY_CURRENT_ITEMS = "current_items"; - - /** - * Creates and returns bundle using workspace items - */ - public static Bundle convertDataModelToAppTargetBundle(Context context, BgDataModel dataModel) { - Bundle bundle = new Bundle(); - ArrayList events = new ArrayList<>(); - ArrayList workspaceItems = dataModel.getAllWorkspaceItems(); - for (ItemInfo item : workspaceItems) { - AppTarget target = getAppTargetFromItemInfo(context, item); - if (target != null && !isTrackedForHotseatPrediction(item)) continue; - events.add(wrapAppTargetWithItemLocation(target, AppTargetEvent.ACTION_PIN, item)); - } - ArrayList currentTargets = new ArrayList<>(); - FixedContainerItems hotseatItems = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION); - if (hotseatItems != null) { - for (ItemInfo itemInfo : hotseatItems.items) { - AppTarget target = getAppTargetFromItemInfo(context, itemInfo); - if (target != null) currentTargets.add(target); - } - } - bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, events); - bundle.putParcelableArrayList(BUNDLE_KEY_CURRENT_ITEMS, currentTargets); - return bundle; - } -} diff --git a/quickstep/src/com/android/launcher3/icons/LauncherIconProviderImpl.kt b/quickstep/src/com/android/launcher3/icons/LauncherIconProviderImpl.kt new file mode 100644 index 0000000000..f67c9541ac --- /dev/null +++ b/quickstep/src/com/android/launcher3/icons/LauncherIconProviderImpl.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageItemInfo +import android.content.res.Resources.NotFoundException +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import com.android.launcher3.LauncherModel +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.graphics.ShapeDelegate.Circle +import com.android.launcher3.graphics.ThemeManager +import com.android.launcher3.icons.cache.CachingLogic +import com.android.launcher3.icons.cache.LauncherActivityCachingLogic +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.PluginManagerWrapper +import com.android.systemui.plugins.IconProcessorPlugin +import com.android.systemui.plugins.PluginLifecycleManager +import com.android.systemui.plugins.PluginListener +import javax.inject.Inject +import javax.inject.Provider + +/** Extension of LauncherIconProvider with system APIs and plugin support */ +private const val TAG = "LauncherIconProviderImpl" + +@LauncherAppSingleton +class LauncherIconProviderImpl +@Inject +constructor( + @ApplicationContext ctx: Context, + themeManager: ThemeManager, + private val modelProvider: Provider, + private val iconCacheProvider: Provider, + pluginManagerWrapper: PluginManagerWrapper, + lifecycle: DaggerSingletonTracker, +) : LauncherIconProvider(ctx, themeManager), PluginListener { + + init { + pluginManagerWrapper.addPluginListener(this, IconProcessorPlugin::class.java) + lifecycle.addCloseable { pluginManagerWrapper.removePluginListener(this) } + } + + private var processor: IconProcessorPlugin? = null + + override fun getApplicationInfoHash(appInfo: ApplicationInfo): String = + (appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode + + override fun loadPackageIcon( + info: PackageItemInfo, + appInfo: ApplicationInfo, + density: Int, + ): Drawable? { + fun Drawable.preprocess(resId: Int) = + processor?.preprocessDrawable(this, resId, appInfo) ?: this + + try { + val resources = mContext.packageManager.getResourcesForApplication(appInfo) + // Try to load the package item icon first + if (info !== appInfo && info.icon != 0) { + try { + val icon = resources.getDrawableForDensity(info.icon, density, null) + if (icon != null) return icon.preprocess(info.icon) + } catch (_: NotFoundException) {} + } + // Load the fallback app icon + if (appInfo.icon != 0) { + // Tries to load the round icon res, if the app defines it as an adaptive icon + if (mThemeManager.iconShape is Circle) { + if (appInfo.roundIconRes != 0 && appInfo.roundIconRes != appInfo.icon) { + try { + val d = + resources.getDrawableForDensity(appInfo.roundIconRes, density, null) + if (d is AdaptiveIconDrawable) return d.preprocess(appInfo.roundIconRes) + } catch (_: NotFoundException) {} + } + } + + try { + return resources + .getDrawableForDensity(appInfo.icon, density, null) + ?.preprocess(appInfo.icon) + } catch (_: NotFoundException) {} + } + } catch (_: Exception) {} + return null + } + + override fun onPluginLoaded( + plugin: IconProcessorPlugin?, + pluginContext: Context?, + manager: PluginLifecycleManager?, + ) { + plugin?.setIconChangeNotifier { pkg, userHandle -> + modelProvider.get().onAppIconChanged(pkg, userHandle) + } + processor = plugin + Log.d(TAG, "Plugin connected $plugin") + MODEL_EXECUTOR.execute { + iconCacheProvider.get().clearMemoryCache() + modelProvider.get().reloadIfActive() + } + } + + override fun onPluginUnloaded( + plugin: IconProcessorPlugin?, + manager: PluginLifecycleManager?, + ) { + processor = null + Log.d(TAG, "Plugin disconnected") + } + + override fun notifyIconLoaded(icon: BitmapInfo, key: ComponentKey, logic: CachingLogic<*>) { + if (logic == LauncherActivityCachingLogic) + processor?.notifyAppIconLoaded(key.componentName, key.user, icon.flags) + } +} diff --git a/quickstep/src/com/android/launcher3/model/AppEventProducer.java b/quickstep/src/com/android/launcher3/model/AppEventProducer.java index 8ca1cd9e3d..880f1fd969 100644 --- a/quickstep/src/com/android/launcher3/model/AppEventProducer.java +++ b/quickstep/src/com/android/launcher3/model/AppEventProducer.java @@ -22,9 +22,8 @@ import static android.app.prediction.AppTargetEvent.ACTION_UNDISMISS; import static android.app.prediction.AppTargetEvent.ACTION_UNPIN; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; -import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_DRAGDROP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO; @@ -42,6 +41,7 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP; +import static com.android.launcher3.model.PredictionHelper.getLocationString; import static com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction; import static com.android.launcher3.model.PredictionHelper.isTrackedForWidgetPrediction; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; @@ -65,10 +65,6 @@ import androidx.annotation.WorkerThread; import com.android.launcher3.Utilities; import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.logger.LauncherAtom.ContainerInfo; -import com.android.launcher3.logger.LauncherAtom.FolderContainer; -import com.android.launcher3.logger.LauncherAtom.HotseatContainer; -import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer; import com.android.launcher3.logging.StatsLogManager.EventEnum; import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutRequest; @@ -76,7 +72,6 @@ import com.android.launcher3.util.UserIconInfo; import com.android.quickstep.logging.StatsLogCompatManager.StatsLogConsumer; import com.android.systemui.shared.system.SysUiStatsLog; -import java.util.Locale; import java.util.Optional; import java.util.function.ObjIntConsumer; @@ -121,7 +116,10 @@ public class AppEventProducer implements StatsLogConsumer { // TODO: remove the running test check when b/231648228 is fixed. if (target != null && !Utilities.isRunningInTestHarness()) { AppTargetEvent event = new AppTargetEvent.Builder(target, eventId) - .setLaunchLocation(getContainer(locationInfo)) + .setLaunchLocation(getLocationString( + locationInfo.getContainerInfo(), + locationInfo.getWidget().getSpanX(), + locationInfo.getWidget().getSpanY())) .build(); mMessageHandler.obtainMessage(MSG_LAUNCH, targetPredictor, 0, event).sendToTarget(); } @@ -135,10 +133,10 @@ public class AppEventProducer implements StatsLogConsumer { || event == LAUNCHER_QUICKSWITCH_RIGHT || event == LAUNCHER_QUICKSWITCH_LEFT || event == LAUNCHER_APP_LAUNCH_DRAGDROP) { - sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION); + sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_ALL_APPS_PREDICTION); } else if (event == LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST || event == LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP) { - sendEvent(atomInfo, ACTION_DISMISS, CONTAINER_PREDICTION); + sendEvent(atomInfo, ACTION_DISMISS, CONTAINER_ALL_APPS_PREDICTION); } else if (event == LAUNCHER_ITEM_DRAG_STARTED) { mLastDragItem = atomInfo; } else if (event == LAUNCHER_ITEM_DROP_COMPLETED) { @@ -183,9 +181,9 @@ public class AppEventProducer implements StatsLogConsumer { AppTarget target = new AppTarget.Builder(new AppTargetId("launcher:launcher"), mContext.getPackageName(), Process.myUserHandle()) .build(); - sendEvent(target, atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION); + sendEvent(target, atomInfo, ACTION_LAUNCH, CONTAINER_ALL_APPS_PREDICTION); } else { - sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION); + sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_ALL_APPS_PREDICTION); } } else if (event == LAUNCHER_DISMISS_PREDICTION_UNDO) { sendEvent(atomInfo, ACTION_UNDISMISS, CONTAINER_HOTSEAT_PREDICTION); @@ -277,68 +275,6 @@ public class AppEventProducer implements StatsLogConsumer { } } - private String getContainer(LauncherAtom.ItemInfo info) { - ContainerInfo ci = info.getContainerInfo(); - switch (ci.getContainerCase()) { - case WORKSPACE: { - // In case the item type is not widgets, the spaceX and spanY default to 1. - int spanX = info.getWidget().getSpanX(); - int spanY = info.getWidget().getSpanY(); - return getWorkspaceContainerString(ci.getWorkspace(), spanX, spanY); - } - case HOTSEAT: { - return getHotseatContainerString(ci.getHotseat()); - } - case TASK_SWITCHER_CONTAINER: { - return "task-switcher"; - } - case ALL_APPS_CONTAINER: { - return "all-apps"; - } - case PREDICTED_HOTSEAT_CONTAINER: { - return "predictions/hotseat"; - } - case PREDICTION_CONTAINER: { - return "predictions"; - } - case SHORTCUTS_CONTAINER: { - return "deep-shortcuts"; - } - case TASK_BAR_CONTAINER: { - return "taskbar"; - } - case FOLDER: { - FolderContainer fc = ci.getFolder(); - switch (fc.getParentContainerCase()) { - case WORKSPACE: - return "folder/" + getWorkspaceContainerString(fc.getWorkspace(), 1, 1); - case HOTSEAT: - return "folder/" + getHotseatContainerString(fc.getHotseat()); - } - return "folder"; - } - case SEARCH_RESULT_CONTAINER: - return "search-results"; - case EXTENDED_CONTAINERS: { - if (ci.getExtendedContainers().getContainerCase() - == DEVICE_SEARCH_RESULT_CONTAINER) { - return "search-results"; - } - } - default: // fall out - } - return ""; - } - - private static String getWorkspaceContainerString(WorkspaceContainer wc, int spanX, int spanY) { - return String.format(Locale.ENGLISH, "workspace/%d/[%d,%d]/[%d,%d]", - wc.getPageIndex(), wc.getGridX(), wc.getGridY(), spanX, spanY); - } - - private static String getHotseatContainerString(HotseatContainer hc) { - return String.format(Locale.ENGLISH, "hotseat/%1$d/[%1$d,0]/[1,1]", hc.getIndex()); - } - private static ComponentName parseNullable(String componentNameString) { return TextUtils.isEmpty(componentNameString) ? null : ComponentName.unflattenFromString(componentNameString); diff --git a/quickstep/src/com/android/launcher3/model/PredictedItemFactory.kt b/quickstep/src/com/android/launcher3/model/PredictedItemFactory.kt new file mode 100644 index 0000000000..47959bf086 --- /dev/null +++ b/quickstep/src/com/android/launcher3/model/PredictedItemFactory.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model + +import android.app.prediction.AppPredictionContext +import android.app.prediction.AppPredictionManager +import android.app.prediction.AppPredictor +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetEvent +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.ShortcutInfo +import android.os.UserHandle +import android.os.UserManager +import androidx.annotation.VisibleForTesting +import com.android.launcher3.LauncherModel +import com.android.launcher3.LauncherModel.ModelUpdateTask +import com.android.launcher3.LauncherSettings.Favorites +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.icons.IconCache +import com.android.launcher3.icons.cache.CacheLookupFlag +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.pm.UserCache +import com.android.launcher3.shortcuts.ShortcutKey +import com.android.launcher3.shortcuts.ShortcutRequest +import com.android.launcher3.util.ApiWrapper +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.PackageManagerHelper +import com.android.launcher3.util.PersistedItemArray +import com.android.launcher3.util.PersistedItemArray.ItemFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** [PersistedItemArray.ItemFactory] to parse predicted items */ +class PredictedItemFactory +@AssistedInject +constructor( + @ApplicationContext private val context: Context, + private val pmHelper: PackageManagerHelper, + private val userCache: UserCache, + private val apiWrapper: ApiWrapper, + private val iconCache: IconCache, + @Assisted private val maxItemCount: Int, + @Assisted private val predictorState: PredictorState, +) : ItemFactory { + + private val quietModeCache = mutableMapOf() + // Number of items persisted can be different than what is needed if the grid changed between + // the two operations + private var readCount = 0 + + override fun createInfo(itemType: Int, user: UserHandle, intent: Intent): ItemInfo? { + if (readCount >= maxItemCount) { + return null + } + when (itemType) { + Favorites.ITEM_TYPE_APPLICATION -> { + val lai = + context + .getSystemService(LauncherApps::class.java) + ?.resolveActivity(intent, user) ?: return null + val info = + AppInfo( + lai, + userCache.getUserInfo(user), + apiWrapper, + pmHelper, + quietModeCache.getOrPut(user) { + context + .getSystemService(UserManager::class.java) + ?.isQuietModeEnabled(user) ?: true + }, + ) + info.container = predictorState.containerId + iconCache.getTitleAndIcon(info, lai, predictorState.lookupFlag) + readCount++ + return info.makeWorkspaceItem(context) + } + + Favorites.ITEM_TYPE_DEEP_SHORTCUT -> { + val key = ShortcutKey.fromIntent(intent, user) ?: return null + val si: ShortcutInfo = + key.buildRequest(context).query(ShortcutRequest.PINNED).getOrNull(0) + ?: return null + val wii = WorkspaceItemInfo(si, context) + wii.container = predictorState.containerId + iconCache.getShortcutIcon(wii, si) + readCount++ + return wii + } + } + return null + } + + @AssistedFactory + interface Factory { + fun newParser(maxItemCount: Int, predictorState: PredictorState): PredictedItemFactory + } +} + +/** Class to manage predicted items for a particular prediction type */ +class PredictorState( + @JvmField val containerId: Int, + storageName: String, + @JvmField val lookupFlag: CacheLookupFlag, +) { + + @JvmField val storage: PersistedItemArray = PersistedItemArray(storageName) + + @VisibleForTesting var predictor: AppPredictor? = null + + private var lastTargets: List = emptyList() + + /** Creates and registers a predictor, and destroys any previously created predictor */ + fun registerPredictor( + ctx: Context, + predictionContext: AppPredictionContext, + model: LauncherModel, + taskFactory: (PredictorState, List) -> ModelUpdateTask, + ) { + destroyPredictor() + + val apm = ctx.getSystemService(AppPredictionManager::class.java) ?: return + lastTargets = emptyList() + + predictor = + apm.createAppPredictionSession(predictionContext).apply { + registerPredictionUpdates(MODEL_EXECUTOR) { + val oldTargets = lastTargets + lastTargets = it + + // If no diff, skip + if ( + oldTargets.size != lastTargets.size || + oldTargets.zip(lastTargets).any { (a1, a2) -> + !areAppTargetsSame(a1, a2) + } + ) { + model.enqueueModelUpdateTask(taskFactory.invoke(this@PredictorState, it)) + } + } + requestPredictionUpdate() + } + } + + /** Destroys a previously created predictor */ + fun destroyPredictor() { + predictor?.destroy() + predictor = null + } + + /** see [AppPredictor.requestPredictionUpdate] */ + fun requestPredictionUpdate() = predictor?.requestPredictionUpdate() + + /** see [AppPredictor.notifyAppTargetEvent] */ + fun notifyAppTargetEvent(event: AppTargetEvent) = predictor?.notifyAppTargetEvent(event) + + /** Compares two targets for the properties which we care about */ + private fun areAppTargetsSame(t1: AppTarget, t2: AppTarget): Boolean { + if ( + (t1.packageName != t2.packageName) || + (t1.user != t2.user) || + (t1.className != t2.className) + ) { + return false + } + return t1.shortcutInfo?.id == t2.shortcutInfo?.id + } +} diff --git a/quickstep/src/com/android/launcher3/model/PredictionHelper.java b/quickstep/src/com/android/launcher3/model/PredictionHelper.java deleted file mode 100644 index dbd99e1eab..0000000000 --- a/quickstep/src/com/android/launcher3/model/PredictionHelper.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.model; - -import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.WORKSPACE; - -import android.app.prediction.AppTarget; -import android.app.prediction.AppTargetEvent; -import android.app.prediction.AppTargetId; -import android.content.ComponentName; -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.android.launcher3.LauncherSettings; -import com.android.launcher3.Workspace; -import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.LauncherAppWidgetInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.shortcuts.ShortcutKey; - -import java.util.Locale; - -/** Helper class with methods for converting launcher items to form usable by predictors */ -public final class PredictionHelper { - private static final String APP_LOCATION_HOTSEAT = "hotseat"; - private static final String APP_LOCATION_WORKSPACE = "workspace"; - - /** - * Creates and returns an {@link AppTarget} object for an {@link ItemInfo}. Returns null - * if item type is not supported in predictions - */ - @Nullable - public static AppTarget getAppTargetFromItemInfo(Context context, ItemInfo info) { - if (info == null) return null; - if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET - && info instanceof LauncherAppWidgetInfo - && ((LauncherAppWidgetInfo) info).providerName != null) { - ComponentName cn = ((LauncherAppWidgetInfo) info).providerName; - return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()), - cn.getPackageName(), info.user).setClassName(cn.getClassName()).build(); - } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION - && info.getTargetComponent() != null) { - ComponentName cn = info.getTargetComponent(); - return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()), - cn.getPackageName(), info.user).setClassName(cn.getClassName()).build(); - } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT - && info instanceof WorkspaceItemInfo) { - ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info); - //TODO: switch to using full shortcut info - return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()), - shortcutKey.componentName.getPackageName(), shortcutKey.user).build(); - } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { - return new AppTarget.Builder(new AppTargetId("folder:" + info.id), - context.getPackageName(), info.user).build(); - } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) { - return new AppTarget.Builder(new AppTargetId("app_pair:" + info.id), - context.getPackageName(), info.user).build(); - } - return null; - } - - /** - * Creates and returns {@link AppTargetEvent} from an {@link AppTarget}, action, and item - * location using {@link ItemInfo} - */ - public static AppTargetEvent wrapAppTargetWithItemLocation( - AppTarget target, int action, ItemInfo info) { - String location = String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]", - info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT - ? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE, - info.screenId, info.cellX, info.cellY, info.spanX, info.spanY); - return new AppTargetEvent.Builder(target, action).setLaunchLocation(location).build(); - } - - /** - * Helper method to determine if {@link ItemInfo} should be tracked and reported to hotseat - * predictors - */ - public static boolean isTrackedForHotseatPrediction(ItemInfo info) { - return info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || ( - info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP - && info.screenId == Workspace.FIRST_SCREEN_ID); - } - - /** - * Helper method to determine if {@link LauncherAtom.ItemInfo} should be tracked and reported to - * hotseat predictors - */ - public static boolean isTrackedForHotseatPrediction(LauncherAtom.ItemInfo info) { - LauncherAtom.ContainerInfo ci = info.getContainerInfo(); - switch (ci.getContainerCase()) { - case HOTSEAT: - return true; - case WORKSPACE: - return ci.getWorkspace().getPageIndex() == 0; - default: - return false; - } - } - - /** - * Helper method to determine if {@link ItemInfo} should be tracked and reported to widget - * predictors - */ - public static boolean isTrackedForWidgetPrediction(ItemInfo info) { - return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET - && info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP; - } - - /** - * Helper method to determine if {@link LauncherAtom.ItemInfo} should be tracked and reported - * to widget predictors - */ - public static boolean isTrackedForWidgetPrediction(LauncherAtom.ItemInfo info) { - return info.getItemCase() == LauncherAtom.ItemInfo.ItemCase.WIDGET - && info.getContainerInfo().getContainerCase() == WORKSPACE; - } -} diff --git a/quickstep/src/com/android/launcher3/model/PredictionHelper.kt b/quickstep/src/com/android/launcher3/model/PredictionHelper.kt new file mode 100644 index 0000000000..122cbd1d60 --- /dev/null +++ b/quickstep/src/com/android/launcher3/model/PredictionHelper.kt @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model + +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetEvent +import android.app.prediction.AppTargetId +import android.content.Context +import android.os.Bundle +import com.android.launcher3.LauncherSettings.Favorites +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION +import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET +import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID +import com.android.launcher3.logger.LauncherAtom +import com.android.launcher3.logger.LauncherAtom.ContainerInfo +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.ALL_APPS_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.EXTENDED_CONTAINERS +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.HOTSEAT +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.PREDICTED_HOTSEAT_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.PREDICTION_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SHORTCUTS_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.TASK_BAR_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.TASK_SWITCHER_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.WORKSPACE +import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase +import com.android.launcher3.logger.LauncherAtom.HotseatContainer +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.WIDGET +import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer +import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.shortcuts.ShortcutKey + +/** Helper class with methods for converting launcher items to form usable by predictors */ +object PredictionHelper { + const val BUNDLE_KEY_PIN_EVENTS = "pin_events" + const val BUNDLE_KEY_CURRENT_ITEMS = "current_items" + const val BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets" + + /** + * Helper method to determine if [ItemInfo] should be tracked and reported to hotseat predictors + */ + @JvmStatic + fun ItemInfo.isTrackedForHotseatPrediction(): Boolean = + container == CONTAINER_HOTSEAT || + (container == CONTAINER_DESKTOP && screenId == FIRST_SCREEN_ID) + + /** + * Helper method to determine if [LauncherAtom.ItemInfo] should be tracked and reported to + * hotseat predictors + */ + @JvmStatic + fun LauncherAtom.ItemInfo.isTrackedForHotseatPrediction(): Boolean = + containerInfo.run { + when (containerCase) { + HOTSEAT -> true + WORKSPACE -> workspace.pageIndex == 0 + else -> false + } + } + + /** + * Helper method to determine if [ItemInfo] should be tracked and reported to widget predictors + */ + @JvmStatic + fun ItemInfo.isTrackedForWidgetPrediction(): Boolean = + itemType == ITEM_TYPE_APPWIDGET && container == CONTAINER_DESKTOP + + /** + * Helper method to determine if [LauncherAtom.ItemInfo] should be tracked and reported to + * widget predictors + */ + @JvmStatic + fun LauncherAtom.ItemInfo.isTrackedForWidgetPrediction(): Boolean = + itemCase == WIDGET && containerInfo.containerCase == WORKSPACE + + /** Creates and returns bundle using workspace items for hotseat predictions */ + @JvmStatic + fun getBundleForHotseatPredictions(context: Context, dataModel: BgDataModel): Bundle { + return Bundle().apply { + putParcelableArrayList( + BUNDLE_KEY_PIN_EVENTS, + dataModel.getPinItemEvents(context) { it.isTrackedForHotseatPrediction() }, + ) + putParcelableArrayList( + BUNDLE_KEY_CURRENT_ITEMS, + dataModel.itemsIdMap + .getPredictedContents(CONTAINER_HOTSEAT_PREDICTION) + .mapNotNullTo(ArrayList()) { getAppTargetFromItemInfo(context, it) }, + ) + } + } + + /** Creates and returns bundle using workspace items for widget predictions */ + @JvmStatic + fun getBundleForWidgetPredictions(context: Context, dataModel: BgDataModel) = + Bundle().apply { + putParcelableArrayList( + BUNDLE_KEY_ADDED_APP_WIDGETS, + dataModel.getPinItemEvents(context) { it.isTrackedForWidgetPrediction() }, + ) + } + + /** Returns a location string to be used with [AppTargetEvent] for the [ContainerInfo] */ + @JvmStatic + fun ContainerInfo.getLocationString(spanX: Int, spanY: Int): String = + when (containerCase) { + // In case the item type is not widgets, the spaceX and spanY default to 1. + WORKSPACE -> workspace.getLocationString(spanX, spanY) + HOTSEAT -> hotseat.getLocationString() + FOLDER -> + folder.let { + when (it.parentContainerCase) { + ParentContainerCase.WORKSPACE -> + "folder/" + it.workspace.getLocationString(1, 1) + ParentContainerCase.HOTSEAT -> "folder/" + it.hotseat.getLocationString() + else -> "folder" + } + } + TASK_SWITCHER_CONTAINER -> "task-switcher" + ALL_APPS_CONTAINER -> "all-apps" + PREDICTED_HOTSEAT_CONTAINER -> "predictions/hotseat" + PREDICTION_CONTAINER -> "predictions" + SHORTCUTS_CONTAINER -> "deep-shortcuts" + TASK_BAR_CONTAINER -> "taskbar" + SEARCH_RESULT_CONTAINER -> "search-results" + EXTENDED_CONTAINERS -> + if (extendedContainers.containerCase == DEVICE_SEARCH_RESULT_CONTAINER) + "search-results" + else "" + else -> "" + } + + private fun WorkspaceContainer.getLocationString(spanX: Int, spanY: Int) = + "workspace/$pageIndex/[$gridX,$gridY]/[$spanX,$spanY]" + + private fun HotseatContainer.getLocationString() = "hotseat/$index/[$index,0]/[1,1]" + + /** + * Creates [AppTargetEvent] for every item in [BgDataModel] with [AppTargetEvent.ACTION_PIN] and + * item location using [ItemInfo] + */ + private fun BgDataModel.getPinItemEvents(context: Context, filter: (ItemInfo) -> Boolean) = + itemsIdMap.filter(filter).mapNotNullTo(ArrayList()) { info -> + getAppTargetFromItemInfo(context, info)?.let { target -> + AppTargetEvent.Builder(target, AppTargetEvent.ACTION_PIN) + .setLaunchLocation(info.containerInfo.getLocationString(info.spanX, info.spanY)) + .build() + } + } + + /** + * Creates and returns an [AppTarget] object for an [ItemInfo]. Returns null if item type is not + * supported in predictions + */ + private fun getAppTargetFromItemInfo(context: Context, info: ItemInfo?): AppTarget? { + if (info == null) return null + return when { + info.itemType == ITEM_TYPE_APPWIDGET && info is LauncherAppWidgetInfo -> + info.providerName?.let { cn -> + AppTarget.Builder( + AppTargetId("widget:" + cn.packageName), + cn.packageName, + info.user, + ) + .setClassName(cn.className) + .build() + } + info.itemType == Favorites.ITEM_TYPE_APPLICATION -> + info.targetComponent?.let { cn -> + AppTarget.Builder( + AppTargetId("app:" + cn.packageName), + cn.packageName, + info.user, + ) + .setClassName(cn.className) + .build() + } + + info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT && info is WorkspaceItemInfo -> + // TODO: switch to using full shortcut info + ShortcutKey.fromItemInfo(info).let { shortcutKey -> + AppTarget.Builder( + AppTargetId("shortcut:" + shortcutKey.id), + shortcutKey.componentName.packageName, + shortcutKey.user, + ) + .build() + } + info.itemType == Favorites.ITEM_TYPE_FOLDER -> + AppTarget.Builder(AppTargetId("folder:" + info.id), context.packageName, info.user) + .build() + info.itemType == Favorites.ITEM_TYPE_APP_PAIR -> + AppTarget.Builder( + AppTargetId("app_pair:" + info.id), + context.packageName, + info.user, + ) + .build() + else -> null + } + } +} diff --git a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java index d604742979..1b6050d000 100644 --- a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java @@ -31,16 +31,16 @@ import android.os.UserHandle; import androidx.annotation.NonNull; import com.android.launcher3.ConstantItem; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel.ModelUpdateTask; import com.android.launcher3.LauncherPrefs; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; -import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; +import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -64,15 +64,16 @@ public class PredictionUpdateTask implements ModelUpdateTask { @Override public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel, @NonNull AllAppsList apps) { - LauncherAppState app = taskController.getApp(); - Context context = app.getContext(); + IconCache iconCache = taskController.getIconCache(); + Context context = taskController.getContext(); // TODO: remove this LauncherPrefs.get(context).put(LAST_PREDICTION_ENABLED, !mTargets.isEmpty()); Set usersForChangedShortcuts = - dataModel.extraItems.get(mPredictorState.containerId).items.stream() - .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT) + dataModel.itemsIdMap.getPredictedContents(mPredictorState.containerId).stream() + .filter(info -> info != null && + info.itemType == ITEM_TYPE_DEEP_SHORTCUT) .map(info -> info.user) .collect(Collectors.toSet()); @@ -83,7 +84,7 @@ public class PredictionUpdateTask implements ModelUpdateTask { if (si != null) { usersForChangedShortcuts.add(si.getUserHandle()); itemInfo = new WorkspaceItemInfo(si, context); - app.getIconCache().getShortcutIcon(itemInfo, si); + iconCache.getShortcutIcon(itemInfo, si); } else { String className = target.getClassName(); if (COMPONENT_CLASS_MARKER.equals(className)) { @@ -95,7 +96,7 @@ public class PredictionUpdateTask implements ModelUpdateTask { itemInfo = apps.data.stream() .filter(info -> user.equals(info.user) && cn.equals(info.componentName)) .map(ai -> { - app.getIconCache().getTitleAndIcon(ai, false); + iconCache.getTitleAndIcon(ai, mPredictorState.lookupFlag); return ai.makeWorkspaceItem(context); }) .findAny() @@ -106,7 +107,7 @@ public class PredictionUpdateTask implements ModelUpdateTask { return null; } AppInfo ai = new AppInfo(context, lai, user); - app.getIconCache().getTitleAndIcon(ai, lai, false); + iconCache.getTitleAndIcon(ai, lai, mPredictorState.lookupFlag); return ai.makeWorkspaceItem(context); }); @@ -119,13 +120,12 @@ public class PredictionUpdateTask implements ModelUpdateTask { items.add(itemInfo); } - FixedContainerItems fci = new FixedContainerItems(mPredictorState.containerId, items); - dataModel.extraItems.put(fci.containerId, fci); - taskController.bindExtraContainerItems(fci); - usersForChangedShortcuts.forEach( - u -> dataModel.updateShortcutPinnedState(app.getContext(), u)); + PredictedContainerInfo pci = new PredictedContainerInfo(mPredictorState.containerId, items); + dataModel.updateAndDispatchItem(pci /* item */, null /* owner */); + taskController.bindUpdatedWorkspaceItems(Collections.singleton(pci)); + usersForChangedShortcuts.forEach(u -> dataModel.updateShortcutPinnedState(context, u)); // Save to disk - mPredictorState.storage.write(context, fci.items); + mPredictorState.storage.write(context, pci.getContents()); } } diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 51af4debc2..39e83328c6 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -20,14 +20,13 @@ import static android.text.format.DateUtils.formatElapsedTime; import static com.android.launcher3.EncryptionType.ENCRYPTED; import static com.android.launcher3.LauncherPrefs.nonRestorableItem; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; -import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; -import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; -import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle; -import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo; -import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation; +import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG; +import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; +import static com.android.launcher3.model.PredictionHelper.getBundleForHotseatPredictions; +import static com.android.launcher3.model.PredictionHelper.getBundleForWidgetPredictions; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.Manifest; @@ -35,9 +34,6 @@ import static java.util.stream.Collectors.toCollection; import android.app.StatsManager; import android.app.prediction.AppPredictionContext; -import android.app.prediction.AppPredictionManager; -import android.app.prediction.AppPredictor; -import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.content.Context; import android.content.Intent; @@ -47,12 +43,10 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.os.Bundle; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import android.util.StatsEvent; -import androidx.annotation.AnyThread; -import androidx.annotation.CallSuper; -import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -63,32 +57,25 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.Utilities; import com.android.launcher3.LauncherPrefs; -import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.dagger.ApplicationContext; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; -import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; +import com.android.launcher3.model.data.WorkspaceData; import com.android.launcher3.pm.UserCache; -import com.android.launcher3.shortcuts.ShortcutKey; -import com.android.launcher3.util.ApiWrapper; -import com.android.launcher3.util.Executors; import com.android.launcher3.util.IntSparseArrayMap; -import com.android.launcher3.util.PackageManagerHelper; -import com.android.launcher3.util.PersistedItemArray; import com.android.quickstep.logging.SettingsChangeLogger; import com.android.quickstep.logging.StatsLogCompatManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.systemui.shared.system.SysUiStatsLog; import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.IntStream; + +import javax.inject.Inject; +import javax.inject.Named; import app.lawnchair.LawnchairApp; import app.lawnchair.compat.LawnchairQuickstepCompat; @@ -98,7 +85,6 @@ import app.lawnchair.compat.LawnchairQuickstepCompat; */ public class QuickstepModelDelegate extends ModelDelegate { - private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; private static final boolean IS_DEBUG = false; @@ -108,107 +94,66 @@ public class QuickstepModelDelegate extends ModelDelegate { nonRestorableItem("LAST_SNAPSHOT_TIME_MILLIS", 0L, ENCRYPTED); @VisibleForTesting - final PredictorState mAllAppsState = new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions"); + final PredictorState mAllPredictionAppsState = new PredictorState( + CONTAINER_ALL_APPS_PREDICTION, "all_apps_predictions", DEFAULT_LOOKUP_FLAG); @VisibleForTesting - final PredictorState mHotseatState = new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions"); + final PredictorState mHotseatPredictionState = new PredictorState( + CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions", DESKTOP_ICON_FLAG); @VisibleForTesting - final PredictorState mWidgetsRecommendationState = new PredictorState(CONTAINER_WIDGETS_PREDICTION, - "widgets_prediction"); + final PredictorState mWidgetsRecommendationState = new PredictorState( + CONTAINER_WIDGETS_PREDICTION, "widgets_prediction", DESKTOP_ICON_FLAG); private final InvariantDeviceProfile mIDP; + private final UserCache mUserCache; + private final PredictedItemFactory.Factory mItemParserFactory; private final AppEventProducer mAppEventProducer; - private StatsManager mStatsManager; + + private final StatsManager mStatsManager; protected boolean mActive = false; - public QuickstepModelDelegate(Context context) { + @Inject + public QuickstepModelDelegate(@ApplicationContext Context context, + InvariantDeviceProfile idp, + UserCache userCache, + PredictedItemFactory.Factory itemParserFactory, + @Nullable @Named("ICONS_DB") String dbFileName) { super(context); + + + mIDP = idp; + mUserCache = userCache; + mItemParserFactory = itemParserFactory; + mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent); - - mIDP = InvariantDeviceProfile.INSTANCE.get(context); StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer); - try { - mStatsManager = context.getSystemService(StatsManager.class); - } catch (Throwable e) { - Log.e(TAG, "Failed to get StatsManager", e); - } + + // Only register for launcher snapshot logging if this is the primary ModelDelegate + // instance, as there will be additional instances that may be destroyed at any time. + mStatsManager = TextUtils.isEmpty(dbFileName) + ? null : context.getSystemService(StatsManager.class); } - @CallSuper @Override - public void loadAndBindWorkspaceItems(@NonNull UserManagerState ums, - @NonNull BgDataModel.Callbacks[] callbacks, - @NonNull Map pinnedShortcuts) { - loadAndBindItems(ums, pinnedShortcuts, callbacks, mIDP.numDatabaseHotseatIcons, - mHotseatState); - } + public void loadAndAddExtraModelItems(@NonNull IntSparseArrayMap outLoadedItems) { + loadAndBindPredictedItems( + mIDP.numDatabaseHotseatIcons, mHotseatPredictionState, outLoadedItems); + loadAndBindPredictedItems(mIDP.numDatabaseAllAppsColumns, mAllPredictionAppsState, + outLoadedItems); - @CallSuper - @Override - public void loadAndBindAllAppsItems(@NonNull UserManagerState ums, - @NonNull BgDataModel.Callbacks[] callbacks, - @NonNull Map pinnedShortcuts) { - loadAndBindItems(ums, pinnedShortcuts, callbacks, mIDP.numDatabaseAllAppsColumns, - mAllAppsState); - } - - @WorkerThread - private void loadAndBindItems(@NonNull UserManagerState ums, - @NonNull Map pinnedShortcuts, - @NonNull BgDataModel.Callbacks[] callbacks, - int numColumns, @NonNull PredictorState state) { - // TODO: Implement caching and preloading - - WorkspaceItemFactory factory = - new WorkspaceItemFactory(mApp, ums, mPmHelper, pinnedShortcuts, numColumns, - state.containerId); - FixedContainerItems fci = new FixedContainerItems(state.containerId, - state.storage.read(mApp.getContext(), factory, ums.allUsers::get)); - if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) { - bindPredictionItems(callbacks, fci); - } - mDataModel.extraItems.put(state.containerId, fci); - } - - @CallSuper - @Override - public void loadAndBindOtherItems(@NonNull BgDataModel.Callbacks[] callbacks) { - FixedContainerItems widgetPredictionFCI = new FixedContainerItems( + // Widgets prediction isn't used frequently. And thus, it is not persisted on disk. + PredictedContainerInfo widgetPredictionFCI = new PredictedContainerInfo( mWidgetsRecommendationState.containerId, new ArrayList<>()); - - // Widgets prediction isn't used frequently. And thus, it is not persisted on - // disk. - mDataModel.extraItems.put(mWidgetsRecommendationState.containerId, widgetPredictionFCI); - - bindPredictionItems(callbacks, widgetPredictionFCI); - loadStringCache(mDataModel.stringCache); + outLoadedItems.put(mWidgetsRecommendationState.containerId, widgetPredictionFCI); } - @AnyThread - private void bindPredictionItems(@NonNull BgDataModel.Callbacks[] callbacks, - @NonNull FixedContainerItems fci) { - Executors.MAIN_EXECUTOR.execute(() -> { - for (BgDataModel.Callbacks c : callbacks) { - c.bindExtraContainerItems(fci); - } - }); - } - - @CallSuper - @Override @WorkerThread - public void bindAllModelExtras(@NonNull BgDataModel.Callbacks[] callbacks) { - Iterable containerItems; - synchronized (mDataModel.extraItems) { - containerItems = mDataModel.extraItems.clone(); - } - Executors.MAIN_EXECUTOR.execute(() -> { - for (BgDataModel.Callbacks c : callbacks) { - for (FixedContainerItems fci : containerItems) { - c.bindExtraContainerItems(fci); - } - } - }); + private void loadAndBindPredictedItems(int numColumns, + @NonNull PredictorState state, @NonNull IntSparseArrayMap outLoadedItems) { + PredictedItemFactory parser = mItemParserFactory.newParser(numColumns, state); + PredictedContainerInfo fci = new PredictedContainerInfo(state.containerId, + state.storage.read(mContext, parser, mUserCache::getUserForSerialNumber)); + outLoadedItems.put(state.containerId, fci); } public void markActive() { @@ -216,9 +161,12 @@ public class QuickstepModelDelegate extends ModelDelegate { mActive = true; } + @WorkerThread @Override public void workspaceLoadComplete() { super.workspaceLoadComplete(); + // Initialize ContextualSearchStateManager. + ContextualSearchStateManager.INSTANCE.get(mContext); recreatePredictors(); } @@ -228,7 +176,7 @@ public class QuickstepModelDelegate extends ModelDelegate { super.modelLoadComplete(); // Log snapshot of the model - LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext()); + LauncherPrefs prefs = LauncherPrefs.get(mContext); long lastSnapshotTimeMillis = prefs.get(LAST_SNAPSHOT_TIME_MILLIS); // Log snapshot only if previous snapshot was older than a day long now = System.currentTimeMillis(); @@ -240,39 +188,30 @@ public class QuickstepModelDelegate extends ModelDelegate { elapsedTime)); } } else { - IntSparseArrayMap itemsIdMap; + WorkspaceData itemsIdMap; synchronized (mDataModel) { - itemsIdMap = mDataModel.itemsIdMap.clone(); + itemsIdMap = mDataModel.itemsIdMap.copy(); } InstanceId instanceId = new InstanceIdSequence().newInstanceId(); for (ItemInfo info : itemsIdMap) { CollectionInfo parent = getContainer(info, itemsIdMap); - StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId); + StatsLogCompatManager.writeSnapshot(info.buildProto(parent, mContext), instanceId); } additionalSnapshotEvents(instanceId); prefs.put(LAST_SNAPSHOT_TIME_MILLIS, now); } - // Only register for launcher snapshot logging if this is the primary - // ModelDelegate - // instance, as there will be additional instances that may be destroyed at any - // time. - if (mIsPrimaryInstance && LawnchairApp.isRecentsEnabled()) { - registerSnapshotLoggingCallback(); - } + registerSnapshotLoggingCallback(); } - protected void additionalSnapshotEvents(InstanceId snapshotInstanceId) { - } + protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){} /** - * Registers a callback to log launcher workspace layout using Statsd pulled - * atom. + * Registers a callback to log launcher workspace layout using Statsd pulled atom. */ - protected void registerSnapshotLoggingCallback() { + private void registerSnapshotLoggingCallback() { if (mStatsManager == null || !LawnchairQuickstepCompat.ATLEAST_R) { - Log.d(TAG, "Failed to get StatsManager"); - return; + Log.d(TAG, "Skipping snapshot logging"); } try { @@ -282,14 +221,14 @@ public class QuickstepModelDelegate extends ModelDelegate { MODEL_EXECUTOR, (i, eventList) -> { InstanceId instanceId = new InstanceIdSequence().newInstanceId(); - IntSparseArrayMap itemsIdMap; + WorkspaceData itemsIdMap; synchronized (mDataModel) { - itemsIdMap = mDataModel.itemsIdMap.clone(); + itemsIdMap = mDataModel.itemsIdMap.copy(); } for (ItemInfo info : itemsIdMap) { CollectionInfo parent = getContainer(info, itemsIdMap); - LauncherAtom.ItemInfo itemInfo = info.buildProto(parent); + LauncherAtom.ItemInfo itemInfo = info.buildProto(parent, mContext); Log.d(TAG, itemInfo.toString()); StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo, instanceId); @@ -298,11 +237,12 @@ public class QuickstepModelDelegate extends ModelDelegate { Log.d(TAG, String.format( "Successfully logged %d workspace items with instanceId=%d", - itemsIdMap.size(), instanceId.getId())); + eventList.size(), instanceId.getId())); additionalSnapshotEvents(instanceId); SettingsChangeLogger.INSTANCE.get(mContext).logSnapshot(instanceId); return StatsManager.PULL_SUCCESS; - }); + } + ); Log.d(TAG, "Successfully registered for launcher snapshot logging!"); } catch (Throwable e) { Log.e(TAG, "Failed to register launcher snapshot logging callback with StatsManager", @@ -311,7 +251,7 @@ public class QuickstepModelDelegate extends ModelDelegate { } private static CollectionInfo getContainer( - ItemInfo info, IntSparseArrayMap itemsIdMap) { + ItemInfo info, WorkspaceData itemsIdMap) { if (info.container > 0) { ItemInfo containerInfo = itemsIdMap.get(info.container); @@ -330,20 +270,17 @@ public class QuickstepModelDelegate extends ModelDelegate { @Override public void validateData() { super.validateData(); - if (mAllAppsState.predictor != null) { - mAllAppsState.predictor.requestPredictionUpdate(); - } - if (mWidgetsRecommendationState.predictor != null) { - mWidgetsRecommendationState.predictor.requestPredictionUpdate(); - } + mAllPredictionAppsState.requestPredictionUpdate(); + mWidgetsRecommendationState.requestPredictionUpdate(); } + @WorkerThread @Override public void destroy() { super.destroy(); mActive = false; StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer); - if (mIsPrimaryInstance && mStatsManager != null && LawnchairQuickstepCompat.ATLEAST_R) { + if (mStatsManager != null && LawnchairQuickstepCompat.ATLEAST_R) { try { mStatsManager.clearPullAtomCallback(SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT); } catch (Throwable e) { @@ -354,8 +291,8 @@ public class QuickstepModelDelegate extends ModelDelegate { } private void destroyPredictors() { - mAllAppsState.destroyPredictor(); - mHotseatState.destroyPredictor(); + mAllPredictionAppsState.destroyPredictor(); + mHotseatPredictionState.destroyPredictor(); mWidgetsRecommendationState.destroyPredictor(); } @@ -365,262 +302,72 @@ public class QuickstepModelDelegate extends ModelDelegate { if (!Utilities.ATLEAST_Q || !mActive) { return; } - Context context = mApp.getContext(); - AppPredictionManager apm = context.getSystemService(AppPredictionManager.class); - if (apm == null) { - return; - } - int usagePerm = mApp.getContext().checkCallingOrSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS); - if (usagePerm != PackageManager.PERMISSION_GRANTED) - return; + mAllPredictionAppsState.registerPredictor(mContext, + new AppPredictionContext.Builder(mContext) + .setUiSurface("home") + .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns) + .build(), + mModel, + PredictionUpdateTask::new); - registerPredictor(mAllAppsState, apm.createAppPredictionSession( - new AppPredictionContext.Builder(context) - .setUiSurface("home") - .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns) - .build())); // TODO: get bundle - registerHotseatPredictor(apm, context); + registerHotseatPredictor(mContext); - registerWidgetsPredictor(apm.createAppPredictionSession( - new AppPredictionContext.Builder(context) - .setUiSurface("widgets") - .setExtras(getBundleForWidgetsOnWorkspace(context, mDataModel)) - .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) - .build())); + mWidgetsRecommendationState.registerPredictor(mContext, + new AppPredictionContext.Builder(mContext) + .setUiSurface("widgets") + .setExtras(getBundleForWidgetPredictions(mContext, mDataModel)) + .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) + .build(), + mModel, + WidgetsPredictionUpdateTask::new); } @WorkerThread private void recreateHotseatPredictor() { - mHotseatState.destroyPredictor(); - if (!mActive) { - return; + mHotseatPredictionState.destroyPredictor(); + if (mActive) { + registerHotseatPredictor(mContext); } - Context context = mApp.getContext(); - AppPredictionManager apm = context.getSystemService(AppPredictionManager.class); - if (apm == null) { - return; - } - registerHotseatPredictor(apm, context); } - private void registerHotseatPredictor(AppPredictionManager apm, Context context) { - registerPredictor(mHotseatState, apm.createAppPredictionSession( + private void registerHotseatPredictor(Context context) { + mHotseatPredictionState.registerPredictor(context, new AppPredictionContext.Builder(context) - .setUiSurface("hotseat") - .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons) - .setExtras(convertDataModelToAppTargetBundle(context, mDataModel)) - .build())); - } - - private void registerPredictor(PredictorState state, AppPredictor predictor) { - state.setTargets(Collections.emptyList()); - state.predictor = predictor; - state.predictor.registerPredictionUpdates( - MODEL_EXECUTOR, new AppPredictor.Callback() { - @Keep - @Override - public void onTargetsAvailable(@NonNull List targets) { - handleUpdate(state, targets); - } - }); - state.predictor.requestPredictionUpdate(); - } - - private void handleUpdate(PredictorState state, List targets) { - if (state.setTargets(targets)) { - // No diff, skip - return; - } - mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets)); - } - - private void registerWidgetsPredictor(AppPredictor predictor) { - mWidgetsRecommendationState.predictor = predictor; - mWidgetsRecommendationState.predictor.registerPredictionUpdates( - MODEL_EXECUTOR, new AppPredictor.Callback() { - @Keep - @Override - public void onTargetsAvailable(@NonNull List targets) { - if (mWidgetsRecommendationState.setTargets(targets)) { - // No diff, skip - return; - } - mApp.getModel().enqueueModelUpdateTask( - new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets)); - } - }); - mWidgetsRecommendationState.predictor.requestPredictionUpdate(); + .setUiSurface("hotseat") + .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons) + .setExtras(getBundleForHotseatPredictions(context, mDataModel)) + .build(), + mModel, PredictionUpdateTask::new); } @VisibleForTesting void onAppTargetEvent(AppTargetEvent event, int client) { PredictorState state; - switch (client) { - case CONTAINER_PREDICTION: - state = mAllAppsState; + switch(client) { + case CONTAINER_ALL_APPS_PREDICTION: + state = mAllPredictionAppsState; break; case CONTAINER_WIDGETS_PREDICTION: state = mWidgetsRecommendationState; break; case CONTAINER_HOTSEAT_PREDICTION: default: - state = mHotseatState; + state = mHotseatPredictionState; break; } - if (state.predictor != null) { - state.predictor.notifyAppTargetEvent(event); - Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction() - + " launchLocation=" + event.getLaunchLocation()); - if (state == mHotseatState - && (event.getAction() == AppTargetEvent.ACTION_PIN - || event.getAction() == AppTargetEvent.ACTION_UNPIN)) { - // Recreate hot seat predictor when we need to query for hot seat due to pin or - // unpin app icons. - recreateHotseatPredictor(); - } - } - } - private Bundle getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel) { - Bundle bundle = new Bundle(); - ArrayList widgetEvents = dataModel.getAllWorkspaceItems().stream() - .filter(PredictionHelper::isTrackedForWidgetPrediction) - .map(item -> { - AppTarget target = getAppTargetFromItemInfo(context, item); - if (target == null) - return null; - return wrapAppTargetWithItemLocation( - target, AppTargetEvent.ACTION_PIN, item); - }) - .filter(Objects::nonNull) - .collect(toCollection(ArrayList::new)); - bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, widgetEvents); - return bundle; - } - - static class PredictorState { - - public final int containerId; - public final PersistedItemArray storage; - public AppPredictor predictor; - - private List mLastTargets; - - PredictorState(int containerId, String storageName) { - this.containerId = containerId; - storage = new PersistedItemArray<>(storageName); - mLastTargets = Collections.emptyList(); - } - - public void destroyPredictor() { - if (predictor != null) { - predictor.destroy(); - predictor = null; - } - } - - /** - * Sets the new targets and returns true if it was the same as before. - */ - boolean setTargets(List newTargets) { - List oldTargets = mLastTargets; - mLastTargets = newTargets; - - int size = oldTargets.size(); - return size == newTargets.size() && IntStream.range(0, size) - .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i))); - } - } - - /** - * Compares two targets for the properties which we care about - */ - private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) { - if (!Objects.equals(t1.getPackageName(), t2.getPackageName()) - || !Objects.equals(t1.getUser(), t2.getUser()) - || !Objects.equals(t1.getClassName(), t2.getClassName())) { - return false; - } - - ShortcutInfo s1 = t1.getShortcutInfo(); - ShortcutInfo s2 = t2.getShortcutInfo(); - if (s1 != null) { - if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) { - return false; - } - } else if (s2 != null) { - return false; - } - return true; - } - - private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory { - - private final LauncherAppState mAppState; - private final UserManagerState mUMS; - private final PackageManagerHelper mPmHelper; - private final Map mPinnedShortcuts; - private final int mMaxCount; - private final int mContainer; - - private int mReadCount = 0; - - protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, - PackageManagerHelper pmHelper, Map pinnedShortcuts, - int maxCount, int container) { - mAppState = appState; - mUMS = ums; - mPmHelper = pmHelper; - mPinnedShortcuts = pinnedShortcuts; - mMaxCount = maxCount; - mContainer = container; - } - - @Nullable - @Override - public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) { - if (mReadCount >= mMaxCount) { - return null; - } - switch (itemType) { - case ITEM_TYPE_APPLICATION: { - LauncherActivityInfo lai = mAppState.getContext() - .getSystemService(LauncherApps.class) - .resolveActivity(intent, user); - if (lai == null) { - return null; - } - AppInfo info = new AppInfo( - lai, - UserCache.INSTANCE.get(mAppState.getContext()).getUserInfo(user), - ApiWrapper.INSTANCE.get(mAppState.getContext()), - mPmHelper, - mUMS.isUserQuiet(user)); - info.container = mContainer; - mAppState.getIconCache().getTitleAndIcon(info, lai, false); - mReadCount++; - return info.makeWorkspaceItem(mAppState.getContext()); - } - case ITEM_TYPE_DEEP_SHORTCUT: { - ShortcutKey key = ShortcutKey.fromIntent(intent, user); - if (key == null) { - return null; - } - ShortcutInfo si = mPinnedShortcuts.get(key); - if (si == null) { - return null; - } - WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext()); - wii.container = mContainer; - mAppState.getIconCache().getShortcutIcon(wii, si); - mReadCount++; - return wii; - } - } - return null; + state.notifyAppTargetEvent(event); + Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction() + + " launchLocation=" + event.getLaunchLocation()); + if (state == mHotseatPredictionState + && (event.getAction() == AppTargetEvent.ACTION_PIN + || event.getAction() == AppTargetEvent.ACTION_UNPIN)) { + // Recreate hot seat predictor when we need to query for hot seat due to pin or + // unpin app icons. + recreateHotseatPredictor(); } } } diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java index a7c965218d..c1d8c01230 100644 --- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java +++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java @@ -44,27 +44,34 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.android.launcher3.R; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.RemoteActionShortcut; import com.android.launcher3.popup.SystemShortcut; +import com.android.launcher3.util.DaggerSingletonObject; +import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.Executors; -import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SimpleBroadcastReceiver; import com.android.launcher3.views.ActivityContext; +import com.android.quickstep.dagger.QuickstepBaseAppComponent; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import javax.inject.Inject; + /** * Data model for digital wellbeing status of apps. */ +@LauncherAppSingleton public final class WellbeingModel implements SafeCloseable { private static final String TAG = "WellbeingModel"; private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000}; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; // Welbeing contract private static final String PATH_ACTIONS = "actions"; @@ -75,18 +82,16 @@ public final class WellbeingModel implements SafeCloseable { private static final String EXTRA_PACKAGES = "packages"; private static final String EXTRA_SUCCESS = "success"; - public static final MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(WellbeingModel::new); + public static final DaggerSingletonObject INSTANCE = + new DaggerSingletonObject<>(QuickstepBaseAppComponent::getWellbeingModel); private final Context mContext; private final String mWellbeingProviderPkg; private final Handler mWorkerHandler; private final ContentObserver mContentObserver; - private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver = - new SimpleBroadcastReceiver(t -> restartObserver()); - private final SimpleBroadcastReceiver mAppAddRemoveReceiver = - new SimpleBroadcastReceiver(this::onAppPackageChanged); + private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver; + private final SimpleBroadcastReceiver mAppAddRemoveReceiver; private final Object mModelLock = new Object(); // Maps the action Id to the corresponding RemoteAction @@ -95,12 +100,19 @@ public final class WellbeingModel implements SafeCloseable { private boolean mIsInTest; - private WellbeingModel(final Context context) { + @Inject + WellbeingModel(@ApplicationContext final Context context, + DaggerSingletonTracker tracker) { mContext = context; mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg); mWorkerHandler = new Handler(TextUtils.isEmpty(mWellbeingProviderPkg) ? Executors.UI_HELPER_EXECUTOR.getLooper() : Executors.getPackageExecutor(mWellbeingProviderPkg).getLooper()); + mWellbeingAppChangeReceiver = + new SimpleBroadcastReceiver(context, mWorkerHandler, t -> restartObserver()); + mAppAddRemoveReceiver = + new SimpleBroadcastReceiver(context, mWorkerHandler, this::onAppPackageChanged); + mContentObserver = new ContentObserver(mWorkerHandler) { @Override @@ -109,8 +121,10 @@ public final class WellbeingModel implements SafeCloseable { } }; mWorkerHandler.post(this::initializeInBackground); + tracker.addCloseable(this); } + @WorkerThread private void initializeInBackground() { if (!TextUtils.isEmpty(mWellbeingProviderPkg)) { mContext.registerReceiver( @@ -134,8 +148,8 @@ public final class WellbeingModel implements SafeCloseable { public void close() { if (!TextUtils.isEmpty(mWellbeingProviderPkg)) { mWorkerHandler.post(() -> { - mWellbeingAppChangeReceiver.unregisterReceiverSafely(mContext); - mAppAddRemoveReceiver.unregisterReceiverSafely(mContext); + mWellbeingAppChangeReceiver.unregisterReceiverSafely(); + mAppAddRemoveReceiver.unregisterReceiverSafely(); mContext.getContentResolver().unregisterContentObserver(mContentObserver); }); } @@ -167,7 +181,7 @@ public final class WellbeingModel implements SafeCloseable { // Work profile apps are not recognized by digital wellbeing. if (userId != UserHandle.myUserId()) { if (DEBUG || mIsInTest) { - Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user"); + Log.w(TAG, "getShortcutForApp [" + packageName + "]: not current user"); } return null; } @@ -177,12 +191,12 @@ public final class WellbeingModel implements SafeCloseable { final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null; if (action == null) { if (DEBUG || mIsInTest) { - Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action"); + Log.w(TAG, "getShortcutForApp [" + packageName + "]: no action"); } return null; } if (DEBUG || mIsInTest) { - Log.d(TAG, + Log.w(TAG, "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle() + "'"); } diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java index 84313965e6..ada7301b42 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java +++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java @@ -16,7 +16,6 @@ package com.android.launcher3.model; -import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; @@ -40,7 +39,6 @@ import androidx.annotation.WorkerThread; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider; @@ -49,7 +47,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -57,72 +54,91 @@ import java.util.stream.Collectors; * Works with app predictor to fetch and process widget predictions displayed in a standalone * widget picker activity for a UI surface. */ -public class WidgetPredictionsRequester { +public class WidgetPredictionsRequester implements AppPredictor.Callback { private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; + // container/screenid/[positionx,positiony]/[spanx,spany] + // Matches the format passed used by PredictionHelper; But, position and size values aren't + // used, so, we pass default values. + @VisibleForTesting + static final String LAUNCH_LOCATION = "workspace/1/[0,0]/[2,2]"; @Nullable private AppPredictor mAppPredictor; private final Context mContext; @NonNull private final String mUiSurface; + private boolean mPredictionsAvailable; + @Nullable + private WidgetPredictionsListener mPredictionsListener = null; + @Nullable Predicate mFilter = null; @NonNull - private final Map> mAllWidgets; + private final Map mAllWidgets; public WidgetPredictionsRequester(Context context, @NonNull String uiSurface, - @NonNull Map> allWidgets) { + @NonNull Map allWidgets) { mContext = context; mUiSurface = uiSurface; mAllWidgets = Collections.unmodifiableMap(allWidgets); } + // AppPredictor.Callback -> onTargetsAvailable + @Override + @WorkerThread + public void onTargetsAvailable(List targets) { + List filteredPredictions = filterPredictions(targets, mAllWidgets, mFilter); + List mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); + + if (!mPredictionsAvailable && mPredictionsListener != null) { + mPredictionsAvailable = true; + MAIN_EXECUTOR.execute( + () -> mPredictionsListener.onPredictionsAvailable(mappedPredictions)); + } + } + /** - * Requests predictions from the app predictions manager and registers the provided callback to - * receive updates when predictions are available. + * Requests one time predictions from the app predictions manager and invokes provided callback + * once predictions are available. Any previous requests may be cancelled. * * @param existingWidgets widgets that are currently added to the surface; - * @param callback consumer of prediction results to be called when predictions are - * available + * @param listener consumer of prediction results to be called when predictions are + * available; any previous listener will no longer receive updates. */ + @WorkerThread // e.g. MODEL_EXECUTOR public void request(List existingWidgets, - Consumer> callback) { - Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface); - Predicate filter = notOnUiSurfaceFilter(existingWidgets); + WidgetPredictionsListener listener) { + clear(); + mPredictionsListener = listener; + mFilter = notOnUiSurfaceFilter(existingWidgets); - MODEL_EXECUTOR.execute(() -> { - clear(); - AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class); - if (apm == null) { - return; - } + AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class); + if (apm == null) { + return; + } - mAppPredictor = apm.createAppPredictionSession( - new AppPredictionContext.Builder(mContext) - .setUiSurface(mUiSurface) - .setExtras(bundle) - .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) - .build()); - mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR, - targets -> bindPredictions(targets, filter, callback)); - mAppPredictor.requestPredictionUpdate(); - }); + Bundle bundle = buildBundleForPredictionSession(existingWidgets); + mAppPredictor = apm.createAppPredictionSession( + new AppPredictionContext.Builder(mContext) + .setUiSurface(mUiSurface) + .setExtras(bundle) + .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) + .build()); + mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR, /*callback=*/ this); + mAppPredictor.requestPredictionUpdate(); } /** * Returns a bundle that can be passed in a prediction session * * @param addedWidgets widgets that are already added by the user in the ui surface - * @param uiSurface a unique identifier of the surface hosting widgets; format - * "widgets_xx"; note - "widgets" is reserved for home screen surface. */ @VisibleForTesting - static Bundle buildBundleForPredictionSession(List addedWidgets, - String uiSurface) { + static Bundle buildBundleForPredictionSession(List addedWidgets) { Bundle bundle = new Bundle(); ArrayList addedAppTargetEvents = new ArrayList<>(); for (AppWidgetProviderInfo info : addedWidgets) { ComponentName componentName = info.provider; - AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName); + AppTargetEvent appTargetEvent = buildAppTargetEvent(info, componentName); addedAppTargetEvents.add(appTargetEvent); } bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents); @@ -134,13 +150,13 @@ public class WidgetPredictionsRequester { * predictor. * Also see {@link PredictionHelper} */ - private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info, + private static AppTargetEvent buildAppTargetEvent(AppWidgetProviderInfo info, ComponentName componentName) { AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName()); AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(), /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build(); - return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) - .setLaunchLocation(uiSurface).build(); + return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN).setLaunchLocation( + LAUNCH_LOCATION).build(); } /** @@ -156,49 +172,26 @@ public class WidgetPredictionsRequester { return widgetItem -> !existingComponentKeys.contains(widgetItem); } - /** Provides the predictions returned by the predictor to the registered callback. */ - @WorkerThread - private void bindPredictions(List targets, Predicate filter, - Consumer> callback) { - List filteredPredictions = filterPredictions(targets, mAllWidgets, filter); - List mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); - - MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); - } - /** * Applies the provided filter (e.g. widgets not on workspace) on the predictions returned by * the predictor. */ @VisibleForTesting static List filterPredictions(List predictions, - Map> allWidgets, Predicate filter) { + @NonNull Map allWidgets, + @Nullable Predicate filter) { List servicePredictedItems = new ArrayList<>(); - List localFilteredWidgets = new ArrayList<>(); for (AppTarget prediction : predictions) { - List widgetsInPackage = allWidgets.get( - new PackageUserKey(prediction.getPackageName(), prediction.getUser())); - if (widgetsInPackage == null || widgetsInPackage.isEmpty()) { - continue; - } String className = prediction.getClassName(); if (!TextUtils.isEmpty(className)) { - WidgetItem item = widgetsInPackage.stream() - .filter(w -> className.equals(w.componentName.getClassName())) - .filter(filter) - .findFirst().orElse(null); - if (item != null) { - servicePredictedItems.add(item); - continue; + WidgetItem widgetItem = allWidgets.get( + new ComponentKey(new ComponentName(prediction.getPackageName(), className), + prediction.getUser())); + if (widgetItem != null && (filter == null || filter.test(widgetItem))) { + servicePredictedItems.add(widgetItem); } } - // No widget was added by the service, try local filtering - widgetsInPackage.stream().filter(filter).findFirst() - .ifPresent(localFilteredWidgets::add); - } - if (servicePredictedItems.isEmpty()) { - servicePredictedItems.addAll(localFilteredWidgets); } return servicePredictedItems; @@ -208,26 +201,34 @@ public class WidgetPredictionsRequester { * Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s. */ private List mapWidgetItemsToItemInfo(List widgetItems) { - List items; - if (enableCategorizedWidgetSuggestions()) { - WidgetRecommendationCategoryProvider categoryProvider = - WidgetRecommendationCategoryProvider.newInstance(mContext); - items = widgetItems.stream() - .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, - categoryProvider.getWidgetRecommendationCategory(mContext, it))) - .collect(Collectors.toList()); - } else { - items = widgetItems.stream().map(it -> new PendingAddWidgetInfo(it.widgetInfo, - CONTAINER_WIDGETS_PREDICTION)).collect(Collectors.toList()); - } - return items; + WidgetRecommendationCategoryProvider categoryProvider = + new WidgetRecommendationCategoryProvider(); + return widgetItems.stream() + .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, + categoryProvider.getWidgetRecommendationCategory(mContext, it))) + .collect(Collectors.toList()); } /** Cleans up any open prediction sessions. */ public void clear() { if (mAppPredictor != null) { + mAppPredictor.unregisterPredictionUpdates(this); mAppPredictor.destroy(); mAppPredictor = null; } + mPredictionsListener = null; + mPredictionsAvailable = false; + mFilter = null; + } + + /** + * Listener class to listen to updates from the {@link WidgetPredictionsRequester} + */ + public interface WidgetPredictionsListener { + /** + * Callback method that is called when the predicted widgets are available. + * @param predictions list of predicted widgets {@link PendingAddWidgetInfo} + */ + void onPredictionsAvailable(List predictions); } } diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java index 64bb05e1ab..e234a1cb60 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java @@ -15,8 +15,12 @@ */ package com.android.launcher3.model; -import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; +import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; +import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; import android.app.prediction.AppTarget; import android.content.ComponentName; @@ -26,18 +30,19 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import com.android.launcher3.LauncherModel.ModelUpdateTask; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; -import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; +import com.android.launcher3.R; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Collectors; /** Task to update model as a result of predicted widgets update */ @@ -60,50 +65,79 @@ public final class WidgetsPredictionUpdateTask implements ModelUpdateTask { @Override public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel, @NonNull AllAppsList apps) { - Set widgetsInWorkspace = dataModel.appWidgets.stream().map( - widget -> new ComponentKey(widget.providerName, widget.user)).collect( - Collectors.toSet()); - Predicate notOnWorkspace = w -> !widgetsInWorkspace.contains(w); - Map allWidgets = - dataModel.widgetsModel.getAllWidgetComponentsWithoutShortcuts(); + Set widgetsInWorkspace = dataModel.itemsIdMap + .stream() + .filter(WIDGET_FILTER) + .map(item -> new ComponentKey(item.getTargetComponent(), item.user)) + .collect(Collectors.toSet()); + + // Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for + // being in predictions. + Map allEligibleWidgets = + dataModel.widgetsModel.getWidgetsByComponentKeyForPicker() + .entrySet() + .stream() + .filter(entry -> entry.getValue().widgetInfo != null + && !widgetsInWorkspace.contains(entry.getValue()) + ).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + + Context context = taskController.getContext(); List servicePredictedItems = new ArrayList<>(); + List addedWidgetApps = new ArrayList<>(); for (AppTarget app : mTargets) { ComponentKey componentKey = new ComponentKey( new ComponentName(app.getPackageName(), app.getClassName()), app.getUser()); - WidgetItem widget = allWidgets.get(componentKey); - if (widget == null) { + WidgetItem widget = allEligibleWidgets.get(componentKey); + if (widget == null) { // widget not eligible. continue; } String className = app.getClassName(); if (!TextUtils.isEmpty(className)) { - if (notOnWorkspace.test(widget)) { - servicePredictedItems.add(widget); - } + servicePredictedItems.add(widget); + addedWidgetApps.add(componentKey.componentName.getPackageName()); } } - List items; - if (enableCategorizedWidgetSuggestions()) { - Context context = taskController.getApp().getContext(); - WidgetRecommendationCategoryProvider categoryProvider = - WidgetRecommendationCategoryProvider.newInstance(context); - items = servicePredictedItems.stream() - .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, - categoryProvider.getWidgetRecommendationCategory(context, it))) - .collect(Collectors.toList()); - } else { - items = servicePredictedItems.stream() - .map(it -> new PendingAddWidgetInfo(it.widgetInfo, - CONTAINER_WIDGETS_PREDICTION)).collect( - Collectors.toList()); - } - FixedContainerItems fixedContainerItems = - new FixedContainerItems(mPredictorState.containerId, items); + int minPredictionCount = context.getResources().getInteger( + R.integer.widget_predictions_min_count); + if (enableTieredWidgetsByDefaultInPicker() + && servicePredictedItems.size() < minPredictionCount) { + // Eligible apps that aren't already part of predictions. + Map> eligibleWidgetsByApp = + allEligibleWidgets.values().stream() + .filter(w -> !addedWidgetApps.contains( + w.componentName.getPackageName())) + .collect(groupingBy(w -> w.componentName.getPackageName())); - dataModel.extraItems.put(mPredictorState.containerId, fixedContainerItems); - taskController.bindExtraContainerItems(fixedContainerItems); + // Randomize available apps list + List appPackages = new ArrayList<>(eligibleWidgetsByApp.keySet()); + Collections.shuffle(appPackages); + + int widgetsToAdd = minPredictionCount - servicePredictedItems.size(); + for (String appPackage : appPackages) { + if (widgetsToAdd <= 0) break; + + List widgetsForApp = eligibleWidgetsByApp.get(appPackage); + int index = new Random().nextInt(widgetsForApp.size()); + // Add a random widget from the app. + servicePredictedItems.add(widgetsForApp.get(index)); + widgetsToAdd--; + } + } + + WidgetRecommendationCategoryProvider categoryProvider = + new WidgetRecommendationCategoryProvider(); + List items = servicePredictedItems.stream() + .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, + categoryProvider.getWidgetRecommendationCategory(context, it))) + .collect(Collectors.toList()); + PredictedContainerInfo pci = + new PredictedContainerInfo(mPredictorState.containerId, items); + + dataModel.updateAndDispatchItem(pci /* item */, null /* owner */); + taskController.bindUpdatedWorkspaceItems(Collections.singleton(pci)); // Don't store widgets prediction to disk because it is not used frequently. } diff --git a/quickstep/src/com/android/launcher3/model/data/TaskViewItemInfo.kt b/quickstep/src/com/android/launcher3/model/data/TaskViewItemInfo.kt new file mode 100644 index 0000000000..c201ab12b6 --- /dev/null +++ b/quickstep/src/com/android/launcher3/model/data/TaskViewItemInfo.kt @@ -0,0 +1,87 @@ +/* + * 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.launcher3.model.data + +import android.content.Context +import android.content.Intent +import android.os.Process +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.PRIVATE +import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag +import com.android.launcher3.LauncherSettings +import com.android.launcher3.logger.LauncherAtom +import com.android.launcher3.pm.UserCache +import com.android.quickstep.TaskUtils +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskView + +class TaskViewItemInfo(taskView: TaskView, taskContainer: TaskContainer?) : WorkspaceItemInfo() { + @VisibleForTesting(otherwise = PRIVATE) val taskViewAtom: LauncherAtom.TaskView + + init { + itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK + container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER + val componentName: String + if (taskContainer != null) { + val componentKey = TaskUtils.getLaunchComponentKeyForTask(taskContainer.task.key) + user = componentKey.user + intent = Intent().setComponent(componentKey.componentName) + title = taskContainer.task.title + if (privateSpaceRestrictAccessibilityDrag()) { + if ( + UserCache.getInstance(taskView.context).getUserInfo(componentKey.user).isPrivate + ) { + runtimeStatusFlags = runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE + } + } + componentName = componentKey.componentName.flattenToShortString() + } else { + user = Process.myUserHandle() + intent = Intent() + componentName = "" + } + + taskViewAtom = + createTaskViewAtom( + type = taskView.type.ordinal, + index = taskView.recentsView?.indexOfChild(taskView) ?: -1, + componentName, + cardinality = taskView.taskContainers.size, + ) + } + + override fun buildProto(cInfo: CollectionInfo?, context: Context): LauncherAtom.ItemInfo = + super.buildProto(cInfo, context).toBuilder().setTaskView(taskViewAtom).build() + + companion object { + @VisibleForTesting(otherwise = PRIVATE) + fun createTaskViewAtom( + type: Int, + index: Int, + componentName: String, + cardinality: Int, + ): LauncherAtom.TaskView = + LauncherAtom.TaskView.newBuilder() + .apply { + this.type = type + this.index = index + this.componentName = componentName + this.cardinality = cardinality + } + .build() + } +} diff --git a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java index 212a5ff050..4293ccddc6 100644 --- a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java +++ b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java @@ -61,6 +61,7 @@ public class ProxyActivityStarter extends Activity { } } catch (NullPointerException | ActivityNotFoundException | SecurityException | SendIntentException e) { + Log.w(TAG, "Proxy activity starter could not start activity: ", e); mParams.deliverResult(this, RESULT_CANCELED, null); } finishAndRemoveTask(); diff --git a/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegateImpl.java similarity index 70% rename from quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java rename to quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegateImpl.java index 8b71f01396..17d33f6153 100644 --- a/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayPredictionsImpl.java +++ b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegateImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,22 +18,23 @@ package com.android.launcher3.secondarydisplay; import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT; import android.content.Context; +import android.window.DesktopExperienceFlags; import com.android.launcher3.appprediction.AppsDividerView; import com.android.launcher3.appprediction.PredictionRowView; -import com.android.launcher3.model.BgDataModel; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.views.ActivityContext; /** - * Implementation of SecondaryDisplayPredictions. + * Implementation of {@link SecondaryDisplayQuickstepDelegate}.. */ @SuppressWarnings("unused") -public final class SecondaryDisplayPredictionsImpl extends SecondaryDisplayPredictions { +public final class SecondaryDisplayQuickstepDelegateImpl extends SecondaryDisplayQuickstepDelegate { private final ActivityContext mActivityContext; private final Context mContext; - public SecondaryDisplayPredictionsImpl(Context context) { + public SecondaryDisplayQuickstepDelegateImpl(Context context) { mContext = context; mActivityContext = ActivityContext.lookupContext(context); } @@ -47,9 +48,14 @@ public final class SecondaryDisplayPredictionsImpl extends SecondaryDisplayPredi } @Override - public void setPredictedApps(BgDataModel.FixedContainerItems item) { + public void setPredictedApps(PredictedContainerInfo info) { mActivityContext.getAppsView().getFloatingHeaderView() .findFixedRowByType(PredictionRowView.class) - .setPredictedApps(item.items); + .setPredictedApps(info.getContents()); + } + + @Override + boolean enableTaskbarConnectedDisplays() { + return DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue(); } } diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java index 2da9c3e89f..dea771bc83 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java @@ -19,6 +19,7 @@ package com.android.launcher3.statehandlers; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH; import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import android.animation.Animator; @@ -29,19 +30,21 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.BaseActivity; -import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.quickstep.util.BaseDepthController; -import com.patrykmichalik.opto.core.PreferenceExtensionsKt; import java.io.PrintWriter; import java.util.function.Consumer; +import com.patrykmichalik.opto.core.PreferenceExtensionsKt; import app.lawnchair.compat.LawnchairQuickstepCompat; import app.lawnchair.preferences2.PreferenceManager2; @@ -50,8 +53,8 @@ import app.lawnchair.preferences2.PreferenceManager2; */ public class DepthController extends BaseDepthController implements StateHandler, BaseActivity.MultiWindowModeChangedListener { - - private final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw; + @VisibleForTesting + final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw; private final Consumer mCrossWindowBlurListener = this::setCrossWindowBlursEnabled; @@ -62,11 +65,16 @@ public class DepthController extends BaseDepthController implements StateHandler private View.OnAttachStateChangeListener mOnAttachListener; + // Ensure {@link mOnDrawListener} is added only once to avoid spamming DragLayer's mRunQueue + // via {@link View#post(Runnable)} + private boolean mIsOnDrawListenerAdded = false; + private boolean mRemoveOnDrawListenerCancelled = false; + private final boolean mEnableDepth; - public DepthController(Launcher l) { - super(l); - var pref = PreferenceManager2.getInstance(l).getWallpaperDepthEffect(); + public DepthController(QuickstepLauncher launcher) { + super(launcher); + var pref = PreferenceManager2.getInstance(launcher).getWallpaperDepthEffect(); mEnableDepth = PreferenceExtensionsKt.firstBlocking(pref); } @@ -75,41 +83,51 @@ public class DepthController extends BaseDepthController implements StateHandler ViewRootImpl viewRootImpl = view.getViewRootImpl(); try { if (Utilities.ATLEAST_Q) { - setSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null); + setBaseSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null); } } catch (Throwable t) { - // Ignore any exceptions + // LC-Ignored } - view.post(() -> view.getViewTreeObserver().removeOnDrawListener(mOnDrawListener)); + mRemoveOnDrawListenerCancelled = false; + view.post(() -> { + if (!mRemoveOnDrawListenerCancelled) { + removeOnDrawListener(); + } + }); } private void ensureDependencies() { - if (mLauncher.getRootView() != null && mOnAttachListener == null) { - View rootView = mLauncher.getRootView(); - mOnAttachListener = new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View view) { - try { - CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(), - mCrossWindowBlurListener); - } catch (Throwable t) { - // Ignore - } - mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); - - // To handle the case where window token is invalid during last setDepth call. - applyDepthAndBlur(); + View rootView = mLauncher.getRootView(); + if (rootView == null) { + return; + } + if (mOnAttachListener != null) { + return; + } + mOnAttachListener = new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + try { + UI_HELPER_EXECUTOR.execute(() -> + CrossWindowBlurListeners.getInstance().addListener( + mLauncher.getMainExecutor(), mCrossWindowBlurListener)); + } catch (Throwable t) { + // LC-Ignored } + mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener); - @Override - public void onViewDetachedFromWindow(View view) { - removeSecondaryListeners(); - } - }; - rootView.addOnAttachStateChangeListener(mOnAttachListener); - if (rootView.isAttachedToWindow()) { - mOnAttachListener.onViewAttachedToWindow(rootView); + // To handle the case where window token is invalid during last setDepth call. + applyDepthAndBlur(); } + + @Override + public void onViewDetachedFromWindow(View view) { + removeSecondaryListeners(); + } + }; + rootView.addOnAttachStateChangeListener(mOnAttachListener); + if (rootView.isAttachedToWindow()) { + mOnAttachListener.onViewAttachedToWindow(rootView); } } @@ -126,12 +144,12 @@ public class DepthController extends BaseDepthController implements StateHandler } private void removeSecondaryListeners() { - if (mCrossWindowBlurListener != null) { - try { - CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener); - } catch (Throwable t) { - // Ignore - } + try { + UI_HELPER_EXECUTOR.execute(() -> + CrossWindowBlurListeners.getInstance() + .removeListener(mCrossWindowBlurListener)); + } catch (Throwable t) { + // LC-Ignored } if (mOpaquenessListener != null) { mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener); @@ -143,10 +161,11 @@ public class DepthController extends BaseDepthController implements StateHandler */ public void setActivityStarted(boolean isStarted) { if (isStarted) { - mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); + addOnDrawListener(); } else { - mLauncher.getDragLayer().getViewTreeObserver().removeOnDrawListener(mOnDrawListener); - setSurface(null); + removeOnDrawListener(); + setBaseSurface(null); + setEarlyWakeup(false); } } @@ -158,13 +177,13 @@ public class DepthController extends BaseDepthController implements StateHandler stateDepth.setValue(toState.getDepth(mLauncher)); if (toState == LauncherState.BACKGROUND_APP) { - mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); + addOnDrawListener(); } } @Override public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config, - PendingAnimation animation) { + PendingAnimation animation) { if (config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER) || mIgnoreStateChangesDuringMultiWindowAnimation) { return; @@ -183,14 +202,32 @@ public class DepthController extends BaseDepthController implements StateHandler super.applyDepthAndBlur(); } } catch (Throwable t) { - // Ignore + // LC-Ignored } } @Override protected void onInvalidSurface() { // Lets wait for surface to become valid again + addOnDrawListener(); + } + + private void addOnDrawListener() { + mRemoveOnDrawListenerCancelled = true; + if (mIsOnDrawListenerAdded) { + return; + } mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener); + mIsOnDrawListenerAdded = true; + } + + private void removeOnDrawListener() { + mRemoveOnDrawListenerCancelled = true; + if (!mIsOnDrawListenerAdded) { + return; + } + mLauncher.getDragLayer().getViewTreeObserver().removeOnDrawListener(mOnDrawListener); + mIsOnDrawListenerAdded = false; } @Override @@ -198,7 +235,7 @@ public class DepthController extends BaseDepthController implements StateHandler mIgnoreStateChangesDuringMultiWindowAnimation = true; ObjectAnimator mwAnimation = ObjectAnimator.ofFloat(stateDepth, MULTI_PROPERTY_VALUE, - mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode)) + mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode)) .setDuration(300); mwAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -214,7 +251,8 @@ public class DepthController extends BaseDepthController implements StateHandler writer.println(prefix + "DepthController"); writer.println(prefix + "\tmMaxBlurRadius=" + mMaxBlurRadius); writer.println(prefix + "\tmCrossWindowBlursEnabled=" + mCrossWindowBlursEnabled); - writer.println(prefix + "\tmSurface=" + mSurface); + writer.println(prefix + "\tmBaseSurface=" + mBaseSurface); + writer.println(prefix + "\tmBaseSurfaceOverride=" + mBaseSurfaceOverride); writer.println(prefix + "\tmStateDepth=" + stateDepth.getValue()); writer.println(prefix + "\tmWidgetDepth=" + widgetDepth.getValue()); writer.println(prefix + "\tmCurrentBlur=" + mCurrentBlur); @@ -224,4 +262,4 @@ public class DepthController extends BaseDepthController implements StateHandler writer.println(prefix + "\tmPauseBlurs=" + mPauseBlurs); writer.println(prefix + "\tmWaitingOnSurfaceValidity=" + mWaitingOnSurfaceValidity); } -} \ No newline at end of file +} diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt new file mode 100644 index 0000000000..826c001132 --- /dev/null +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt @@ -0,0 +1,813 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.statehandlers + +import android.content.Context +import android.os.Debug +import android.util.Log +import android.util.Slog +import android.util.SparseArray +import android.view.Display.DEFAULT_DISPLAY +import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY +import androidx.core.util.forEach +import com.android.internal.util.LatencyTracker +import com.android.launcher3.LauncherState +import com.android.launcher3.R +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.statemanager.BaseState +import com.android.launcher3.statemanager.StatefulActivity +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.util.DaggerSingletonObject +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener +import com.android.quickstep.GestureState.GestureEndTarget +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.fallback.RecentsState +import com.android.wm.shell.desktopmode.DisplayDeskState +import com.android.wm.shell.desktopmode.IDesktopTaskListener.Stub +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useRoundedCorners +import java.io.PrintWriter +import java.lang.ref.WeakReference +import javax.inject.Inject + +/** + * Controls the visibility of the workspace and the resumed / paused state when desktop mode is + * enabled. + */ +@LauncherAppSingleton +class DesktopVisibilityController +@Inject +constructor( + @ApplicationContext private val context: Context, + systemUiProxy: SystemUiProxy, + lifecycleTracker: DaggerSingletonTracker, +) { + /** + * Tracks the desks configurations on each display. + * + * (Used only when multiple desks are enabled). + * + * @property displayId The ID of the display this object represents. + * @property activeDeskId The ID of the active desk on the associated display (if any). It has a + * value of `INACTIVE_DESK_ID` (-1) if there are no active desks. Note that there can only be + * at most one active desk on each display. + * @property deskIds a set containing the IDs of the desks on the associated display. + */ + private data class DisplayDeskConfig( + val displayId: Int, + var activeDeskId: Int = INACTIVE_DESK_ID, + val deskIds: MutableSet, + ) + + /** True if it is possible to create new desks on current setup. */ + var canCreateDesks: Boolean = false + private set(value) { + if (field == value) return + field = value + desktopVisibilityListeners.forEach { it.onCanCreateDesksChanged(field) } + } + + /** Maps each display by its ID to its desks configuration. */ + private val displaysDesksConfigsMap = SparseArray() + + private val desktopVisibilityListeners: MutableSet = HashSet() + private val taskbarDesktopModeListeners: MutableSet = HashSet() + + // This simply indicates that user is currently in desktop mode or not. + @Deprecated("Does not work with multi-desks") private var isInDesktopModeDeprecated = false + + // to track if any pending notification to be done. + var isNotifyingDesktopVisibilityPending = false + + // to let launcher hold off on notifying desktop visibility listeners. + var launcherAnimationRunning = false + + // TODO: b/394387739 - Deprecate this and replace it with something that tracks the count per + // desk. + /** + * Number of visible desktop windows in desktop mode. This can be > 0 when user goes to overview + * from desktop window mode. + */ + @Deprecated("Does not work with multi-desks") + var visibleDesktopTasksCountDeprecated: Int = 0 + /** + * Sets the number of desktop windows that are visible and updates launcher visibility based + * on it. + */ + set(visibleTasksCount) { + if (enableMultipleDesktops(context)) { + return + } + if (DEBUG) { + Log.d( + TAG, + ("setVisibleDesktopTasksCount: visibleTasksCount=" + + visibleTasksCount + + " currentValue=" + + field), + ) + } + + if (visibleTasksCount != field) { + if (visibleDesktopTasksCountDeprecated == 0 && visibleTasksCount == 1) { + isInDesktopModeDeprecated = true + } + if (visibleDesktopTasksCountDeprecated == 1 && visibleTasksCount == 0) { + isInDesktopModeDeprecated = false + } + val wasVisible = field > 0 + val isVisible = visibleTasksCount > 0 + val wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview() + field = visibleTasksCount + val areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview() + + if ( + wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow || + wasVisible != isVisible + ) { + if (!launcherAnimationRunning) { + notifyIsInDesktopModeChanged(DEFAULT_DISPLAY, areDesktopTasksVisibleNow) + } else { + isNotifyingDesktopVisibilityPending = true + } + } + + if ( + !ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue && wasVisible != isVisible + ) { + // TODO: b/333533253 - Remove after flag rollout + if (field > 0) { + if (!inOverviewState) { + // When desktop tasks are visible & we're not in overview, we want + // launcher + // to appear paused, this ensures that taskbar displays. + markLauncherPaused() + } + } else { + // If desktop tasks aren't visible, ensure that launcher appears resumed to + // behave normally. + markLauncherResumed() + } + } + } + } + + private var inOverviewState = false + private var backgroundStateEnabled = false + private var gestureInProgress = false + + private var desktopTaskListener: DesktopTaskListenerImpl? + + init { + desktopTaskListener = DesktopTaskListenerImpl(this, context) + systemUiProxy.setDesktopTaskListener(desktopTaskListener) + + lifecycleTracker.addCloseable { + desktopTaskListener = null + systemUiProxy.setDesktopTaskListener(null) + } + } + + /** + * Returns the ID of the active desk (if any) on the display whose ID is [displayId], or + * [INACTIVE_DESK_ID] if no desk is currently active or the multiple desks feature is disabled. + */ + fun getActiveDeskId(displayId: Int): Int { + if (!enableMultipleDesktops(context)) { + // When the multiple desks feature is disabled, callers should not rely on the concept + // of a desk ID. + return INACTIVE_DESK_ID + } + + return getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID + } + + /** Returns whether a desk is currently active on the display with the given [displayId]. */ + fun isInDesktopMode(displayId: Int): Boolean { + if (!enableMultipleDesktops(context)) { + return isInDesktopModeDeprecated + } + + val activeDeskId = getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID + val isInDesktopMode = activeDeskId != INACTIVE_DESK_ID + if (DEBUG) { + Log.d(TAG, "isInDesktopMode: $isInDesktopMode") + } + return isInDesktopMode + } + + /** + * Returns whether a desk is currently active on the display with the given [displayId] and + * Overview is not active. + */ + fun isInDesktopModeAndNotInOverview(displayId: Int): Boolean { + if (!enableMultipleDesktops(context)) { + return areDesktopTasksVisibleAndNotInOverview() + } + + if (DEBUG) { + Log.d(TAG, "isInDesktopModeAndNotInOverview: overview=$inOverviewState") + } + return isInDesktopMode(displayId) && !inOverviewState + } + + /** Whether desktop tasks are visible in desktop mode. */ + private fun areDesktopTasksVisibleAndNotInOverview(): Boolean { + val desktopTasksVisible: Boolean = visibleDesktopTasksCountDeprecated > 0 + if (DEBUG) { + Log.d( + TAG, + ("areDesktopTasksVisible: desktopVisible=" + + desktopTasksVisible + + " overview=" + + inOverviewState), + ) + } + return desktopTasksVisible && !inOverviewState + } + + /** Registers a listener for Taskbar changes in Desktop Mode. */ + fun registerTaskbarDesktopModeListener(listener: TaskbarDesktopModeListener) { + taskbarDesktopModeListeners.add(listener) + } + + /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */ + fun unregisterTaskbarDesktopModeListener(listener: TaskbarDesktopModeListener) { + taskbarDesktopModeListeners.remove(listener) + } + + fun onLauncherStateChanged(state: LauncherState) { + onLauncherStateChanged( + state, + state === LauncherState.BACKGROUND_APP, + state.isRecentsViewVisible, + ) + } + + /** + * Launcher Driven Desktop Mode changes. For example, swipe to home and quick switch from + * Desktop Windowing Mode. if there is any pending notification please notify desktop visibility + * listeners. + */ + fun onLauncherAnimationFromDesktopEnd() { + launcherAnimationRunning = false + if (isNotifyingDesktopVisibilityPending) { + isNotifyingDesktopVisibilityPending = false + notifyIsInDesktopModeChanged( + DEFAULT_DISPLAY, + isInDesktopModeAndNotInOverview(DEFAULT_DISPLAY), + ) + } + } + + fun onLauncherStateChanged(state: RecentsState) { + onLauncherStateChanged( + state, + state === RecentsState.BACKGROUND_APP, + state.isRecentsViewVisible, + ) + } + + /** Process launcher state change and update launcher view visibility based on desktop state */ + fun onLauncherStateChanged( + state: BaseState<*>, + isBackgroundAppState: Boolean, + isRecentsViewVisible: Boolean, + ) { + if (DEBUG) { + Log.d(TAG, "onLauncherStateChanged: newState=$state") + } + setBackgroundStateEnabled(isBackgroundAppState) + // Desktop visibility tracks overview and background state separately + setOverviewStateEnabled(!isBackgroundAppState && isRecentsViewVisible) + } + + private fun setOverviewStateEnabled(overviewStateEnabled: Boolean) { + if (DEBUG) { + Log.d( + TAG, + ("setOverviewStateEnabled: enabled=" + + overviewStateEnabled + + " currentValue=" + + inOverviewState), + ) + } + if (overviewStateEnabled != inOverviewState) { + val wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview() + inOverviewState = overviewStateEnabled + val areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview() + + if (!enableMultipleDesktops(context)) { + if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) { + notifyIsInDesktopModeChanged(DEFAULT_DISPLAY, areDesktopTasksVisibleNow) + } + } else { + // When overview state changes, it changes together on all displays. + displaysDesksConfigsMap.forEach { displayId, deskConfig -> + // Overview affects the state of desks only if desktop mode is active on this + // display. + if (isInDesktopMode(displayId)) { + notifyIsInDesktopModeChanged( + displayId, + isInDesktopModeAndNotInOverview(displayId), + ) + } + } + } + + if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) { + return + } + + // TODO: b/333533253 - Clean up after flag rollout + if (inOverviewState) { + markLauncherResumed() + } else if (areDesktopTasksVisibleNow && !gestureInProgress) { + // Switching out of overview state and gesture finished. + // If desktop tasks are still visible, hide launcher again. + markLauncherPaused() + } + } + } + + /** Registers a listener for Taskbar changes in Desktop Mode. */ + fun registerDesktopVisibilityListener(listener: DesktopVisibilityListener) { + desktopVisibilityListeners.add(listener) + } + + /** Removes a previously registered listener for Taskbar changes in Desktop Mode. */ + fun unregisterDesktopVisibilityListener(listener: DesktopVisibilityListener) { + desktopVisibilityListeners.remove(listener) + } + + private fun notifyIsInDesktopModeChanged( + displayId: Int, + isInDesktopModeAndNotInOverview: Boolean, + ) { + if (DEBUG) { + Log.d( + TAG, + "notifyIsInDesktopModeChanged: displayId=$displayId, isInDesktopModeAndNotInOverview=$isInDesktopModeAndNotInOverview", + ) + } + + for (listener in desktopVisibilityListeners) { + listener.onIsInDesktopModeChanged(displayId, isInDesktopModeAndNotInOverview) + } + } + + private fun notifyTaskbarDesktopModeListeners(doesAnyTaskRequireTaskbarRounding: Boolean) { + if (DEBUG) { + Log.d( + TAG, + "notifyTaskbarDesktopModeListeners: doesAnyTaskRequireTaskbarRounding=" + + doesAnyTaskRequireTaskbarRounding, + ) + } + for (listener in taskbarDesktopModeListeners) { + listener.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding) + } + } + + private fun notifyTaskbarDesktopModeListenersForEntry(duration: Int) { + if (DEBUG) { + Log.d(TAG, "notifyTaskbarDesktopModeListenersForEntry: duration=" + duration) + } + for (listener in taskbarDesktopModeListeners) { + listener.onEnterDesktopMode(duration) + } + DisplayController.INSTANCE.get(context).notifyConfigChange() + } + + private fun notifyTaskbarDesktopModeListenersForExit(duration: Int) { + if (DEBUG) { + Log.d(TAG, "notifyTaskbarDesktopModeListenersForExit: duration=" + duration) + } + for (listener in taskbarDesktopModeListeners) { + listener.onExitDesktopMode(duration) + } + DisplayController.INSTANCE.get(context).notifyConfigChange() + } + + private fun notifyOnDeskAdded(displayId: Int, deskId: Int) { + if (DEBUG) { + Log.d(TAG, "notifyOnDeskAdded: displayId=$displayId, deskId=$deskId") + } + + for (listener in desktopVisibilityListeners) { + listener.onDeskAdded(displayId, deskId) + } + } + + private fun notifyOnDeskRemoved(displayId: Int, deskId: Int) { + if (DEBUG) { + Log.d(TAG, "notifyOnDeskRemoved: displayId=$displayId, deskId=$deskId") + } + + for (listener in desktopVisibilityListeners) { + listener.onDeskRemoved(displayId, deskId) + } + } + + private fun notifyOnActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) { + if (DEBUG) { + Log.d( + TAG, + "notifyOnActiveDeskChanged: displayId=$displayId, newActiveDesk=$newActiveDesk, oldActiveDesk=$oldActiveDesk", + ) + } + + for (listener in desktopVisibilityListeners) { + listener.onActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk) + } + } + + /** TODO: b/333533253 - Remove after flag rollout */ + private fun setBackgroundStateEnabled(backgroundStateEnabled: Boolean) { + if (DEBUG) { + Log.d( + TAG, + ("setBackgroundStateEnabled: enabled=" + + backgroundStateEnabled + + " currentValue=" + + this.backgroundStateEnabled), + ) + } + if (backgroundStateEnabled != this.backgroundStateEnabled) { + this.backgroundStateEnabled = backgroundStateEnabled + if (this.backgroundStateEnabled) { + markLauncherResumed() + } else if (areDesktopTasksVisibleAndNotInOverview() && !gestureInProgress) { + // Switching out of background state. If desktop tasks are visible, pause launcher. + markLauncherPaused() + } + } + } + + var isRecentsGestureInProgress: Boolean + /** + * Whether recents gesture is currently in progress. + * + * TODO: b/333533253 - Remove after flag rollout + */ + get() = gestureInProgress + /** TODO: b/333533253 - Remove after flag rollout */ + private set(gestureInProgress) { + if (gestureInProgress != this.gestureInProgress) { + this.gestureInProgress = gestureInProgress + } + } + + /** + * Notify controller that recents gesture has started. + * + * TODO: b/333533253 - Remove after flag rollout + */ + fun setRecentsGestureStart() { + if (DEBUG) { + Log.d(TAG, "setRecentsGestureStart") + } + isRecentsGestureInProgress = true + } + + /** + * Notify controller that recents gesture finished with the given + * [com.android.quickstep.GestureState.GestureEndTarget] + * + * TODO: b/333533253 - Remove after flag rollout + */ + fun setRecentsGestureEnd(endTarget: GestureEndTarget?) { + if (DEBUG) { + Log.d(TAG, "setRecentsGestureEnd: endTarget=$endTarget") + } + isRecentsGestureInProgress = false + + if (endTarget == null) { + // Gesture did not result in a new end target. Ensure launchers gets paused again. + markLauncherPaused() + } + } + + private fun onListenerConnected( + displayDeskStates: Array, + canCreateDesks: Boolean, + ) { + if (!enableMultipleDesktops(context)) { + return + } + + displaysDesksConfigsMap.clear() + + displayDeskStates.forEach { displayDeskState -> + displaysDesksConfigsMap[displayDeskState.displayId] = + DisplayDeskConfig( + displayId = displayDeskState.displayId, + activeDeskId = displayDeskState.activeDeskId, + deskIds = displayDeskState.deskIds.toMutableSet(), + ) + } + + this.canCreateDesks = canCreateDesks + } + + private fun getDisplayDeskConfig(displayId: Int) = + displaysDesksConfigsMap[displayId] + ?: null.also { Slog.e(TAG, "Expected non-null desk config for display: $displayId") } + + private fun onCanCreateDesksChanged(canCreateDesks: Boolean) { + if (!enableMultipleDesktops(context)) { + return + } + + this.canCreateDesks = canCreateDesks + } + + private fun onDeskAdded(displayId: Int, deskId: Int) { + if (!enableMultipleDesktops(context)) { + return + } + + // Add the config for the desk if there is nothing yet, as the display can start without any + // desks. + if (getDisplayDeskConfig(displayId) == null) { + displaysDesksConfigsMap[displayId] = + DisplayDeskConfig(displayId, INACTIVE_DESK_ID, mutableSetOf(deskId)) + } else { + getDisplayDeskConfig(displayId)!!.also { + check(it.deskIds.add(deskId)) { + "Found a duplicate desk Id: $deskId on display: $displayId" + } + } + } + + notifyOnDeskAdded(displayId, deskId) + } + + private fun onDeskRemoved(displayId: Int, deskId: Int) { + if (!enableMultipleDesktops(context)) { + return + } + + getDisplayDeskConfig(displayId)?.also { + check(it.deskIds.remove(deskId)) { + "Removing non-existing desk Id: $deskId on display: $displayId" + } + if (it.activeDeskId == deskId) { + it.activeDeskId = INACTIVE_DESK_ID + } + } + + notifyOnDeskRemoved(displayId, deskId) + } + + private fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) { + if (!enableMultipleDesktops(context)) { + return + } + + getDisplayDeskConfig(displayId)?.also { + check(oldActiveDesk == it.activeDeskId) { + "Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}" + } + check(newActiveDesk == INACTIVE_DESK_ID || it.deskIds.contains(newActiveDesk)) { + "newActiveDesk: $newActiveDesk was never added to display: $displayId" + } + it.activeDeskId = newActiveDesk + } + + if (newActiveDesk != oldActiveDesk) { + notifyOnActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk) + } + + if ( + (newActiveDesk == INACTIVE_DESK_ID || oldActiveDesk == INACTIVE_DESK_ID) && + !launcherAnimationRunning + ) { + val duration = context.resources.getInteger(R.integer.to_desktop_animation_duration_ms) + if (oldActiveDesk == INACTIVE_DESK_ID && newActiveDesk != INACTIVE_DESK_ID) { + notifyTaskbarDesktopModeListenersForEntry(duration) + } else if (newActiveDesk == INACTIVE_DESK_ID && oldActiveDesk != INACTIVE_DESK_ID) { + notifyTaskbarDesktopModeListenersForExit(duration) + } else { + // do nothing because user switch between two desktop. + } + } else { + isNotifyingDesktopVisibilityPending = true + } + } + + /** TODO: b/333533253 - Remove after flag rollout */ + private fun markLauncherPaused() { + if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) { + return + } + if (DEBUG) { + Log.d(TAG, "markLauncherPaused " + Debug.getCaller()) + } + val activity: StatefulActivity? = + QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext() + activity?.setPaused() + } + + /** TODO: b/333533253 - Remove after flag rollout */ + private fun markLauncherResumed() { + if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) { + return + } + if (DEBUG) { + Log.d(TAG, "markLauncherResumed " + Debug.getCaller()) + } + val activity: StatefulActivity? = + QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext() + // Check activity state before calling setResumed(). Launcher may have been actually + // paused (eg fullscreen task moved to front). + // In this case we should not mark the activity as resumed. + if (activity != null && activity.isResumed) { + activity.setResumed() + } + } + + fun dumpLogs(prefix: String, pw: PrintWriter) { + pw.println(prefix + "DesktopVisibilityController:") + + pw.println("$prefix\tdesktopVisibilityListeners=$desktopVisibilityListeners") + pw.println("$prefix\tvisibleDesktopTasksCount=$visibleDesktopTasksCountDeprecated") + pw.println("$prefix\tinOverviewState=$inOverviewState") + pw.println("$prefix\tbackgroundStateEnabled=$backgroundStateEnabled") + pw.println("$prefix\tgestureInProgress=$gestureInProgress") + pw.println("$prefix\tdesktopTaskListener=$desktopTaskListener") + pw.println("$prefix\tcontext=$context") + } + + /** + * Wrapper for the IDesktopTaskListener stub to prevent lingering references to the launcher + * activity via the controller. + */ + private class DesktopTaskListenerImpl( + controller: DesktopVisibilityController, + @ApplicationContext private val context: Context, + ) : Stub() { + private val controller = WeakReference(controller) + private val displayId = context.displayId + + override fun onListenerConnected( + displayDeskStates: Array, + canCreateDesks: Boolean, + ) { + MAIN_EXECUTOR.execute { + controller.get()?.onListenerConnected(displayDeskStates, canCreateDesks) + } + } + + @Deprecated("Not needed by multi-desks") + override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { + if (enableMultipleDesktops(context)) return + if (displayId != this.displayId) return + MAIN_EXECUTOR.execute { + controller.get()?.apply { + if (DEBUG) { + Log.d(TAG, "desktop visible tasks count changed=$visibleTasksCount") + } + visibleDesktopTasksCountDeprecated = visibleTasksCount + } + } + } + + override fun onStashedChanged(displayId: Int, stashed: Boolean) { + Log.w(TAG, "DesktopTaskListenerImpl: onStashedChanged is deprecated") + } + + override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) { + if (!useRoundedCorners()) return + MAIN_EXECUTOR.execute { + controller.get()?.apply { + Log.d( + TAG, + "DesktopTaskListenerImpl: doesAnyTaskRequireTaskbarRounding= " + + doesAnyTaskRequireTaskbarRounding, + ) + notifyTaskbarDesktopModeListeners(doesAnyTaskRequireTaskbarRounding) + } + } + } + + @Deprecated("Not needed by multi-desks") + override fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) { + if (enableMultipleDesktops(context)) return + val controller = controller.get() ?: return + MAIN_EXECUTOR.execute { + Log.d( + TAG, + ("DesktopTaskListenerImpl: onEnterDesktopModeTransitionStarted with " + + "duration= " + + transitionDuration), + ) + if (!controller.isInDesktopModeDeprecated) { + controller.isInDesktopModeDeprecated = true + controller.notifyTaskbarDesktopModeListenersForEntry(transitionDuration) + } + } + } + + @Deprecated("Not needed by multi-desks") + override fun onExitDesktopModeTransitionStarted( + transitionDuration: Int, + shouldEndUpAtHome: Boolean, + ) { + if (enableMultipleDesktops(context)) return + val controller = controller.get() ?: return + MAIN_EXECUTOR.execute { + Log.d( + TAG, + ("DesktopTaskListenerImpl: onExitDesktopModeTransitionStarted with " + + "duration= " + + transitionDuration), + ) + // If shouldEndUpAtHome is true, desktop mode is ending from the user + // closing/minimizing the last open window. If it's false, the display is + // probably transitioning to an app's full screen mode instead so this metric + // should not be logged. + if (shouldEndUpAtHome) { + LatencyTracker.getInstance(context) + .onActionStart( + LatencyTracker.ACTION_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE + ) + } + if (controller.isInDesktopModeDeprecated) { + controller.isInDesktopModeDeprecated = false + controller.notifyTaskbarDesktopModeListenersForExit(transitionDuration) + } + } + } + + override fun onCanCreateDesksChanged(canCreateDesks: Boolean) { + MAIN_EXECUTOR.execute { controller.get()?.onCanCreateDesksChanged(canCreateDesks) } + } + + override fun onDeskAdded(displayId: Int, deskId: Int) { + MAIN_EXECUTOR.execute { controller.get()?.onDeskAdded(displayId, deskId) } + } + + override fun onDeskRemoved(displayId: Int, deskId: Int) { + MAIN_EXECUTOR.execute { controller.get()?.onDeskRemoved(displayId, deskId) } + } + + override fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) { + MAIN_EXECUTOR.execute { + controller.get()?.onActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk) + } + } + } + + /** A listener for Taskbar in Desktop Mode. */ + interface TaskbarDesktopModeListener { + /** + * Callback for when task is resized in desktop mode. + * + * @param doesAnyTaskRequireTaskbarRounding whether task requires taskbar corner roundness. + */ + fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {} + + /** + * Callback for when user is exiting desktop mode. + * + * @param duration for exit transition + */ + fun onExitDesktopMode(duration: Int) {} + + /** + * Callback for when user is entering desktop mode. + * + * @param duration for enter transition + */ + fun onEnterDesktopMode(duration: Int) {} + } + + companion object { + @JvmField + val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getDesktopVisibilityController) + + private const val TAG = "DesktopVisController" + private const val DEBUG = false + + const val INACTIVE_DESK_ID = -1 + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt b/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt new file mode 100644 index 0000000000..ad847b47ee --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt @@ -0,0 +1,182 @@ +/* + * 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.launcher3.taskbar + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.content.Context +import android.view.View +import androidx.dynamicanimation.animation.SpringForce +import com.android.app.animation.Interpolators +import com.android.launcher3.LauncherAnimUtils +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X +import com.android.launcher3.anim.SpringAnimationBuilder +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +/** Animator helper that creates bars animators. */ +object BarsLocationAnimatorHelper { + const val FADE_OUT_ANIM_ALPHA_DURATION_MS: Long = 50L + const val FADE_OUT_ANIM_ALPHA_DELAY_MS: Long = 50L + const val FADE_OUT_ANIM_POSITION_DURATION_MS: Long = 100L + const val FADE_IN_ANIM_ALPHA_DURATION_MS: Long = 100L + + // Use STIFFNESS_MEDIUMLOW which is not defined in the API constants + private const val FADE_IN_ANIM_POSITION_SPRING_STIFFNESS: Float = 400f + + // During fade out animation we shift the bubble bar 1/80th of the screen width + private const val FADE_OUT_ANIM_POSITION_SHIFT: Float = 1 / 80f + + // During fade in animation we shift the bubble bar 1/60th of the screen width + private const val FADE_IN_ANIM_POSITION_SHIFT: Float = 1 / 60f + + private val Context.screenWidth: Int + get() = resources.displayMetrics.widthPixels + + val Context.outShift: Float + get() = screenWidth * FADE_OUT_ANIM_POSITION_SHIFT + + val Context.inShiftX: Float + get() = screenWidth * FADE_IN_ANIM_POSITION_SHIFT + + /** + * Creates out animation for targetView that animates it finalTx and plays targetViewAlphaAnim + * to its final value. + */ + private fun createLocationOutAnimator( + finalTx: Float, + targetViewAlphaAnim: ObjectAnimator, + targetView: View, + ): Animator { + val positionAnim = + ObjectAnimator.ofFloat(targetView, VIEW_TRANSLATE_X, finalTx) + .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS) + positionAnim.interpolator = Interpolators.EMPHASIZED_ACCELERATE + + targetViewAlphaAnim.setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS) + targetViewAlphaAnim.startDelay = FADE_OUT_ANIM_ALPHA_DELAY_MS + + val animatorSet = AnimatorSet() + animatorSet.playTogether(positionAnim, targetViewAlphaAnim) + return animatorSet + } + + /** + * Creates in animation for targetView that animates it from startTx to finalTx and plays + * targetViewAlphaAnim to its final value. + */ + private fun createLocationInAnimator( + startTx: Float, + finalTx: Float, + targetViewAlphaAnim: ObjectAnimator, + targetView: View, + ): Animator { + targetViewAlphaAnim.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS) + val positionAnim: ValueAnimator = + SpringAnimationBuilder(targetView.context) + .setStartValue(startTx) + .setEndValue(finalTx) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS) + .build(targetView, VIEW_TRANSLATE_X) + val animatorSet = AnimatorSet() + animatorSet.playTogether(positionAnim, targetViewAlphaAnim) + return animatorSet + } + + /** Creates an animator for the bubble bar view in part. */ + @JvmStatic + fun getBubbleBarLocationInAnimator( + newLocation: BubbleBarLocation, + currentLocation: BubbleBarLocation, + distanceFromOtherSide: Float, + targetViewAlphaAnim: ObjectAnimator, + bubbleBarView: View, + ): Animator { + val shift: Float = bubbleBarView.context.outShift + + val onLeft = newLocation.isOnLeft(bubbleBarView.isLayoutRtl) + val startTx: Float + val finalTx = + if (newLocation == currentLocation) { + // Animated location matches layout location. + 0f + } else { + // We are animating in to a transient location, need to move the bar + // accordingly. + distanceFromOtherSide * (if (onLeft) -1 else 1) + } + startTx = + if (onLeft) { + // Bar will be shown on the left side. Start point is shifted right. + finalTx + shift + } else { + // Bar will be shown on the right side. Start point is shifted left. + finalTx - shift + } + return createLocationInAnimator(startTx, finalTx, targetViewAlphaAnim, bubbleBarView) + } + + /** + * Creates an animator for the bubble bar view out part. + * + * @param targetLocation the location bubble bar should animate to. + */ + @JvmStatic + fun getBubbleBarLocationOutAnimator( + bubbleBarView: View, + targetLocation: BubbleBarLocation, + targetViewAlphaAnim: ObjectAnimator, + ): Animator { + val onLeft = targetLocation.isOnLeft(bubbleBarView.isLayoutRtl) + val shift = bubbleBarView.context.outShift + val finalTx = bubbleBarView.translationX + (if (onLeft) -shift else shift) + return this.createLocationOutAnimator(finalTx, targetViewAlphaAnim, bubbleBarView) + } + + /** Creates a teleport animator for the navigation buttons view. */ + @JvmStatic + fun getTeleportAnimatorForNavButtons( + location: BubbleBarLocation, + navButtonsView: View, + navBarTargetTranslationX: Float, + ): Animator { + val outShift: Float = navButtonsView.context.outShift + val isNavBarOnRight: Boolean = location.isOnLeft(navButtonsView.isLayoutRtl) + val finalOutTx = + navButtonsView.translationX + (if (isNavBarOnRight) outShift else -outShift) + val fadeout: Animator = + createLocationOutAnimator( + finalOutTx, + ObjectAnimator.ofFloat(navButtonsView, LauncherAnimUtils.VIEW_ALPHA, 0f), + navButtonsView, + ) + val inShift: Float = navButtonsView.context.inShiftX + val inStartX = navBarTargetTranslationX + (if (isNavBarOnRight) -inShift else inShift) + val fadeIn: Animator = + createLocationInAnimator( + inStartX, + navBarTargetTranslationX, + ObjectAnimator.ofFloat(navButtonsView, LauncherAnimUtils.VIEW_ALPHA, 1f), + navButtonsView, + ) + val teleportAnimator = AnimatorSet() + teleportAnimator.play(fadeout).before(fadeIn) + return teleportAnimator + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java index c201236586..a60efcaf3a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java @@ -16,36 +16,117 @@ package com.android.launcher3.taskbar; import android.content.Context; -import android.view.ContextThemeWrapper; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.graphics.Point; +import android.os.UserHandle; import android.view.LayoutInflater; -import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.popup.SystemShortcut; +import com.android.launcher3.util.BaseContext; +import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.Themes; -import com.android.launcher3.views.ActivityContext; - -import java.util.ArrayList; -import java.util.List; +import com.android.quickstep.SystemUiProxy; // TODO(b/218912746): Share more behavior to avoid all apps context depending directly on taskbar. /** Base for common behavior between taskbar window contexts. */ -public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext { +public abstract class BaseTaskbarContext extends BaseContext + implements SystemShortcut.BubbleActivityStarter { + private final int mDisplayId; + private final boolean mIsPrimaryDisplay; protected final LayoutInflater mLayoutInflater; - private final List mDPChangeListeners = new ArrayList<>(); - public BaseTaskbarContext(Context windowContext) { + public BaseTaskbarContext(Context windowContext, int displayId, boolean isPrimaryDisplay) { super(windowContext, Themes.getActivityThemeRes(windowContext)); + mDisplayId = displayId; + mIsPrimaryDisplay = isPrimaryDisplay; mLayoutInflater = LayoutInflater.from(this).cloneInContext(this); } + @Override + public int getDisplayId() { + return mDisplayId; + } + + /** + * Returns whether the taskbar is displayed on primary or external display. + */ + public final boolean isPrimaryDisplay() { + return mIsPrimaryDisplay; + } + + /** + * Returns whether taskbar is transient or persistent. External displays will be persistent. + * + * @return {@code true} if transient, {@code false} if persistent. + */ + public abstract boolean isTransientTaskbar(); + + /** + * Returns whether the taskbar is pinned in gesture navigation mode. + */ + public abstract boolean isPinnedTaskbar(); + + /** + * Returns the current navigation mode. External displays will be in THREE_BUTTONS mode. + */ + public abstract NavigationMode getNavigationMode(); + + /** + * Returns whether the taskbar is in desktop mode. Implies that some desktop tasks are currently + * visible. + */ + public abstract boolean isInDesktopMode(); + + /** + * Returns whether the taskbar is showing desktop tasks, which may happen even outside desktop + * mode on freeform displays. + */ + public abstract boolean isTaskbarShowingDesktopTasks(); + + /** + * Returns whether the taskbar is forced to be pinned when home is visible. + */ + public abstract boolean showLockedTaskbarOnHome(); + + /** + * Returns whether desktop taskbar (pinned taskbar that shows desktop tasks) is to be used on + * the display because the display is a freeform display. + */ + public abstract boolean showDesktopTaskbarForFreeformDisplay(); + + /** + * Returns screen size. + */ + public abstract Point getScreenSize(); + + /** + * Returns display height. + */ + public abstract int getDisplayHeight(); + + /** + * Notifies the context that the configuration has changed. + */ + public abstract void notifyConfigChanged(); + + @Override public final LayoutInflater getLayoutInflater() { return mLayoutInflater; } @Override - public final List getOnDeviceProfileChangeListeners() { - return mDPChangeListeners; + public void showShortcutBubble(ShortcutInfo info) { + if (info == null) return; + SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info); + } + + @Override + public void showAppBubble(Intent intent, UserHandle user) { + if (intent == null || intent.getPackage() == null) return; + SystemUiProxy.INSTANCE.get(this).showAppBubble(intent, user); } /** Callback invoked when a drag is initiated within this context. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java index 06d9ee6c96..2a73d61cd5 100644 --- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java @@ -25,20 +25,27 @@ import androidx.annotation.Nullable; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.statemanager.StateManager; -import com.android.quickstep.RecentsActivity; +import com.android.launcher3.statemanager.StatefulContainer; +import com.android.quickstep.FallbackActivityInterface; +import com.android.quickstep.GestureState; +import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.fallback.RecentsState; -import com.android.quickstep.util.TISBindHelper; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; +import java.io.PrintWriter; import java.util.stream.Stream; /** * A data source which integrates with the fallback RecentsActivity instance (for 3P launchers). + * @param The type of the RecentsViewContainer that will handle Recents state changes. */ -public class FallbackTaskbarUIController extends TaskbarUIController { +public class FallbackTaskbarUIController + > + extends TaskbarUIController { - private final RecentsActivity mRecentsActivity; + private final T mRecentsContainer; private final StateManager.StateListener mStateListener = new StateManager.StateListener() { @@ -46,8 +53,12 @@ public class FallbackTaskbarUIController extends TaskbarUIController { public void onStateTransitionStart(RecentsState toState) { animateToRecentsState(toState); + RecentsView recentsView = getRecentsView(); + if (recentsView == null) { + return; + } // Handle tapping on live tile. - getRecentsView().setTaskLaunchListener(toState == RecentsState.DEFAULT + recentsView.setTaskLaunchListener(toState == RecentsState.DEFAULT ? (() -> animateToRecentsState(RecentsState.BACKGROUND_APP)) : null); } @@ -63,30 +74,37 @@ public class FallbackTaskbarUIController extends TaskbarUIController { } }; - public FallbackTaskbarUIController(RecentsActivity recentsActivity) { - mRecentsActivity = recentsActivity; + public FallbackTaskbarUIController(T recentsContainer) { + mRecentsContainer = recentsContainer; } @Override protected void init(TaskbarControllers taskbarControllers) { super.init(taskbarControllers); - - mRecentsActivity.setTaskbarUIController(this); - mRecentsActivity.getStateManager().addStateListener(mStateListener); + mRecentsContainer.setTaskbarUIController(this); + mRecentsContainer.getStateManager().addStateListener(mStateListener); } @Override protected void onDestroy() { super.onDestroy(); - mRecentsActivity.setTaskbarUIController(null); - mRecentsActivity.getStateManager().removeStateListener(mStateListener); + mRecentsContainer.setTaskbarUIController(null); + mRecentsContainer.getStateManager().removeStateListener(mStateListener); + } + + @Nullable + @Override + public Animator getParallelAnimationToGestureEndTarget(GestureState.GestureEndTarget endTarget, + long duration, RecentsAnimationCallbacks callbacks) { + return createAnimToRecentsState( + FallbackActivityInterface.INSTANCE.stateFromGestureEndTarget(endTarget), duration); } /** * Creates an animation to animate the taskbar for the given state (but does not start it). * Currently this animation just force stashes the taskbar in Overview. */ - public Animator createAnimToRecentsState(RecentsState toState, long duration) { + private Animator createAnimToRecentsState(RecentsState toState, long duration) { // Force stash taskbar (disallow unstashing) when: // - in a 3P launcher or overview task. // - not running in a test harness (unstash is needed for tests) @@ -108,8 +126,8 @@ public class FallbackTaskbarUIController extends TaskbarUIController { } @Override - public RecentsView getRecentsView() { - return mRecentsActivity.getOverviewPanel(); + public @Nullable RecentsView getRecentsView() { + return mRecentsContainer.getOverviewPanel(); } @Override @@ -124,18 +142,21 @@ public class FallbackTaskbarUIController extends TaskbarUIController { private boolean isIn3pHomeOrRecents() { TopTaskTracker.CachedTaskInfo topTask = TopTaskTracker.INSTANCE - .get(mControllers.taskbarActivityContext).getCachedTopTask(true); + .get(mControllers.taskbarActivityContext).getCachedTopTask(true, + mRecentsContainer.asContext().getDisplayId()); return topTask.isHomeTask() || topTask.isRecentsTask(); } - @Nullable - @Override - protected TISBindHelper getTISBindHelper() { - return mRecentsActivity.getTISBindHelper(); - } - @Override protected String getTaskbarUIControllerName() { - return "FallbackTaskbarUIController"; + return "FallbackTaskbarUIController<" + mRecentsContainer.getClass().getSimpleName() + ">"; + } + + @Override + protected void dumpLogs(String prefix, PrintWriter pw) { + super.dumpLogs(prefix, pw); + + pw.println(String.format("%s\tRecentsState=%s", prefix, + mRecentsContainer.getStateManager().getState())); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java index 358d703b75..7174af30bc 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -15,61 +15,87 @@ */ package com.android.launcher3.taskbar; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_OVERFLOW; + +import static com.android.launcher3.taskbar.TaskbarDesktopExperienceFlags.enableAltTabKqsFlatenning; +import static com.android.launcher3.taskbar.TaskbarDesktopExperienceFlags.enableAltTabKqsOnConnectedDisplays; + +import android.app.ActivityManager; import android.content.ComponentName; import android.content.pm.ActivityInfo; +import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; -import com.android.quickstep.LauncherActivityInterface; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer; +import com.android.launcher3.util.TouchController; +import com.android.quickstep.RecentsFilterState; import com.android.quickstep.RecentsModel; import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.util.SingleTask; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.wm.shell.shared.desktopmode.DesktopState; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Handles initialization of the {@link KeyboardQuickSwitchViewController}. */ public final class KeyboardQuickSwitchController implements - TaskbarControllers.LoggableTaskbarController { + TaskbarControllers.LoggableTaskbarController, TouchController { @VisibleForTesting public static final int MAX_TASKS = 6; @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks(); - - // Initialized on init - @Nullable private RecentsModel mModel; + // Callback used to notify when the KQS view is closed. + @Nullable private Runnable mOnClosed; // Used to keep track of the last requested task list id, so that we do not request to load the // tasks again if we have already requested it and the task list has not changed private int mTaskListChangeId = -1; // Only empty before the recent tasks list has been loaded the first time @NonNull private List mTasks = new ArrayList<>(); + // Set of task IDs filtered out of tasks in recents model to generate list of tasks to show in + // the Keyboard Quick Switch view. Non empty only if the view has been shown in response to + // toggling taskbar overflow button. + @NonNull private Set mExcludedTaskIds = Collections.emptySet(); + private int mNumHiddenTasks = 0; // Initialized in init private TaskbarControllers mControllers; + @Nullable private RecentsModel mModel; + private boolean mIsProjectedMode; @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController; + @Nullable private TaskbarOverlayContext mOverlayContext; + + private boolean mHasDesktopTask = false; + private boolean mWasDesktopTaskFilteredOut = false; /** Initialize the controller. */ public void init(@NonNull TaskbarControllers controllers) { mControllers = controllers; mModel = RecentsModel.INSTANCE.get(controllers.taskbarActivityContext); + mIsProjectedMode = DesktopState.fromContext( + mControllers.taskbarActivityContext).isProjectedMode(); } void onConfigurationChanged(@ActivityInfo.Config int configChanges) { @@ -82,10 +108,12 @@ public final class KeyboardQuickSwitchController implements return; } int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex(); + boolean wasOpenedFromTaskbar = mQuickSwitchViewController.wasOpenedFromTaskbar(); onDestroy(); if (currentFocusedIndex != -1) { mControllers.taskbarActivityContext.getMainThreadHandler().post( - () -> openQuickSwitchView(currentFocusedIndex)); + () -> openQuickSwitchView(currentFocusedIndex, mExcludedTaskIds, + wasOpenedFromTaskbar)); } } @@ -93,76 +121,249 @@ public final class KeyboardQuickSwitchController implements openQuickSwitchView(-1); } + /** + * Opens or closes the view in response to taskbar action. The view shows a filtered list of + * tasks. + * @param taskIdsToExclude A list of tasks to exclude in the opened view. + * @param onClosed A callback used to notify when the KQS view is closed. + */ + void toggleQuickSwitchViewForTaskbar(@NonNull Set taskIdsToExclude, + @NonNull Runnable onClosed) { + mOnClosed = onClosed; + + // Close the view if its shown, and was opened from the taskbar. + if (mQuickSwitchViewController != null + && !mQuickSwitchViewController.isCloseAnimationRunning() + && mQuickSwitchViewController.wasOpenedFromTaskbar()) { + closeQuickSwitchView(true); + return; + } + + openQuickSwitchView(-1, taskIdsToExclude, true); + } + private void openQuickSwitchView(int currentFocusedIndex) { + openQuickSwitchView(currentFocusedIndex, Collections.emptySet(), false); + } + + private void openQuickSwitchView(int currentFocusedIndex, + @NonNull Set taskIdsToExclude, + boolean wasOpenedFromTaskbar) { if (mQuickSwitchViewController != null) { if (!mQuickSwitchViewController.isCloseAnimationRunning()) { + if (mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) { + return; + } + + // Relayout the KQS view instead of recreating a new one if it is the current + // trigger surface is different than the previous one. + final int currentFocusIndexOverride = + currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() + ? 0 : currentFocusedIndex; + + // Skip the task reload if the list is not changed. + if (!mModel.isTaskListValid(mTaskListChangeId) || !taskIdsToExclude.equals( + mExcludedTaskIds)) { + final boolean shouldShowDesktopTasks = mControllers.taskbarDesktopModeController + .shouldShowDesktopTasksInTaskbar(); + mExcludedTaskIds = taskIdsToExclude; + mTaskListChangeId = mModel.getTasks( + shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER + : RecentsFilterState.getDesktopTaskFilter(), + (tasks) -> { + processLoadedTasks(wasOpenedFromTaskbar, tasks, taskIdsToExclude); + mQuickSwitchViewController.updateQuickSwitchView( + mTasks, + wasOpenedFromTaskbar ? 0 : mNumHiddenTasks, + currentFocusIndexOverride, + mHasDesktopTask, + mWasDesktopTaskFilteredOut); + }); + } + + mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar, + currentFocusIndexOverride); return; + } else { + // Allow the KQS to be reopened during the close animation to make it more + // responsive. + closeQuickSwitchView(false); } - // Allow the KQS to be reopened during the close animation to make it more responsive - closeQuickSwitchView(false); } - TaskbarOverlayContext overlayContext = - mControllers.taskbarOverlayController.requestWindow(); + + mOverlayContext = mControllers.taskbarOverlayController.requestWindow(); + if (ENABLE_TASKBAR_OVERFLOW.isTrue()) { + mOverlayContext.getDragLayer().addTouchController(this); + } KeyboardQuickSwitchView keyboardQuickSwitchView = - (KeyboardQuickSwitchView) overlayContext.getLayoutInflater() + (KeyboardQuickSwitchView) mOverlayContext.getLayoutInflater() .inflate( R.layout.keyboard_quick_switch_view, - overlayContext.getDragLayer(), + mOverlayContext.getDragLayer(), /* attachToRoot= */ false); mQuickSwitchViewController = new KeyboardQuickSwitchViewController( - mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks); + mControllers, mOverlayContext, keyboardQuickSwitchView, mControllerCallbacks); - DesktopVisibilityController desktopController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - final boolean onDesktop = - desktopController != null && desktopController.areDesktopTasksVisible(); + final boolean shouldShowDesktopTasks = mControllers.taskbarDesktopModeController + .shouldShowDesktopTasksInTaskbar(); - if (mModel.isTaskListValid(mTaskListChangeId)) { + if (mModel.isTaskListValid(mTaskListChangeId) + && taskIdsToExclude.equals(mExcludedTaskIds)) { // When we are opening the KQS with no focus override, check if the first task is // running. If not, focus that first task. mQuickSwitchViewController.openQuickSwitchView( mTasks, - mNumHiddenTasks, + wasOpenedFromTaskbar ? 0 : mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() ? 0 : currentFocusedIndex, - onDesktop); + shouldShowDesktopTasks, + mHasDesktopTask, + mWasDesktopTaskFilteredOut, + wasOpenedFromTaskbar); return; } - mTaskListChangeId = mModel.getTasks((tasks) -> { - if (onDesktop) { - processLoadedTasksOnDesktop(tasks); - } else { - processLoadedTasks(tasks); - } - // Check if the first task is running after the recents model has updated so that we use - // the correct index. - mQuickSwitchViewController.openQuickSwitchView( - mTasks, - mNumHiddenTasks, - /* updateTasks= */ true, - currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() - ? 0 : currentFocusedIndex, - onDesktop); - }); + mExcludedTaskIds = taskIdsToExclude; + mTaskListChangeId = mModel.getTasks( + shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER + : RecentsFilterState.getDesktopTaskFilter(), + (tasks) -> { + processLoadedTasks(wasOpenedFromTaskbar, tasks, taskIdsToExclude); + // Check if the first task is running after the recents model has updated so + // that we use the correct index. + mQuickSwitchViewController.openQuickSwitchView( + mTasks, + wasOpenedFromTaskbar ? 0 : mNumHiddenTasks, + /* updateTasks= */ true, + currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() + ? 0 : currentFocusedIndex, + shouldShowDesktopTasks, + mHasDesktopTask, + mWasDesktopTaskFilteredOut, + wasOpenedFromTaskbar); + }); } - private void processLoadedTasks(List tasks) { + private boolean shouldIncludeTask(GroupTask task, Set taskIdsToExclude) { + return !ENABLE_TASKBAR_OVERFLOW.isTrue() + || task.getTasks().stream().noneMatch(t -> taskIdsToExclude.contains(t.key.id)); + } + + private void processLoadedTasks(boolean openedFromTaskbar, List tasks, + Set taskIdsToExclude) { + mHasDesktopTask = false; + mWasDesktopTaskFilteredOut = false; + + if (enableAltTabKqsFlatenning.isTrue() && !openedFromTaskbar) { + processLoadedTasksCombined(tasks, taskIdsToExclude); + } else if (mControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()) { + processLoadedTasksOnDesktop(tasks, taskIdsToExclude); + } else { + processLoadedTasksOutsideDesktop(tasks, taskIdsToExclude); + } + } + + private void processLoadedTasksCombined(List tasks, Set taskIdsToExclude) { + List allTasks = tasks.stream() + .flatMap(task -> { + // In case of DesktopTasks, convert each contained task into a new DesktopTask + // this way the view controller will be able to show a thumbnail in KQS view. + if (task instanceof DesktopTask desktopTask) { + return desktopTask.getTasks().stream() + .map(singleTask -> new DesktopTask(desktopTask.getDeskId(), + desktopTask.getDisplayId(), List.of(singleTask))); + } + + return Stream.of(task); + }) + .filter(task -> shouldIncludeTask(task, taskIdsToExclude)) + .filter(this::shouldIncludeTaskBasedOnProjectedMode) + .toList(); + + mTasks = allTasks.stream() + .sorted(combinedTasksComparator()) + .limit(MAX_TASKS) + .toList(); + mNumHiddenTasks = Math.max(0, allTasks.size() - MAX_TASKS); + } + + private boolean shouldIncludeTaskBasedOnProjectedMode(GroupTask task) { + // When not in projected mode, include tasks from all displays + if (!mIsProjectedMode) { + return true; + } + + int primaryDisplayId = mControllers.taskbarActivityContext.getPrimaryDisplayId(); + + // When on primary device in projected mode, only show tasks from the primary device. + if (mControllers.taskbarActivityContext.isPrimaryDisplay()) { + return task.getDisplayId() == primaryDisplayId; + } + + // When on connected display with primary device in projected mode, only include tasks that + // are not on primary device. + return task.getDisplayId() != primaryDisplayId; + } + + private static Comparator combinedTasksComparator() { + return Comparator.comparingLong((GroupTask groupTask) -> + groupTask.getTasks().stream() + .map(task -> task.key.lastActiveTime) + .max(Comparator.naturalOrder()) + // Empty tasks list shouldn't be possible so return -1 in that case. + .orElse(-1L)) + .reversed(); + } + + private void processLoadedTasksOutsideDesktop(List tasks, + Set taskIdsToExclude) { // Only store MAX_TASK tasks, from most to least recent Collections.reverse(tasks); mTasks = tasks.stream() + .filter(task -> !(task instanceof DesktopTask) + && shouldIncludeTask(task, taskIdsToExclude)) .limit(MAX_TASKS) .collect(Collectors.toList()); - mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS); + + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i) instanceof DesktopTask) { + mHasDesktopTask = true; + if (i < mTasks.size()) { + mWasDesktopTaskFilteredOut = true; + } + break; + } + } + + mNumHiddenTasks = Math.max(0, + tasks.size() - (mWasDesktopTaskFilteredOut ? 1 : 0) - MAX_TASKS); } - private void processLoadedTasksOnDesktop(List tasks) { - // Find the single desktop task that contains a grouping of desktop tasks - DesktopTask desktopTask = findDesktopTask(tasks); + private void processLoadedTasksOnDesktop(List tasks, Set taskIdsToExclude) { + // Find all desktop tasks. + List desktopTasks = tasks.stream() + .filter(t -> t instanceof DesktopTask) + .map(t -> (DesktopTask) t) + .toList(); - if (desktopTask != null) { - mTasks = desktopTask.tasks.stream().map(GroupTask::new).collect(Collectors.toList()); + // Apps on the connected displays seem to be in different Desktop tasks even with the + // multiple desktops flag disabled. So, until multiple desktops is implemented the following + // should help with team-fooding Alt+tab on connected displays. Post multiple desktop, + // further changes maybe required to support launching selected desktops. + if (enableAltTabKqsOnConnectedDisplays.isTrue()) { + mTasks = desktopTasks.stream() + .flatMap(t -> t.getTasks().stream()) + .map(SingleTask::new) + .filter(task -> shouldIncludeTask(task, taskIdsToExclude)) + .collect(Collectors.toList()); + + mNumHiddenTasks = Math.max(0, tasks.size() - desktopTasks.size()); + } else if (!desktopTasks.isEmpty()) { + mTasks = desktopTasks.get(0).getTasks().stream() + .map(SingleTask::new) + .filter(task -> shouldIncludeTask(task, taskIdsToExclude)) + .collect(Collectors.toList()); // All other tasks, apart from the grouped desktop task, are hidden mNumHiddenTasks = Math.max(0, tasks.size() - 1); } else { @@ -172,14 +373,6 @@ public final class KeyboardQuickSwitchController implements } } - @Nullable - private DesktopTask findDesktopTask(List tasks) { - return (DesktopTask) tasks.stream() - .filter(t -> t instanceof DesktopTask) - .findFirst() - .orElse(null); - } - void closeQuickSwitchView() { closeQuickSwitchView(true); } @@ -201,12 +394,54 @@ public final class KeyboardQuickSwitchController implements ? -1 : mQuickSwitchViewController.launchFocusedTask(); } + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + return false; + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (mQuickSwitchViewController == null + || mOverlayContext == null + || !ENABLE_TASKBAR_OVERFLOW.isTrue()) { + return false; + } + + TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer(); + if (ev.getAction() == MotionEvent.ACTION_DOWN + && !mQuickSwitchViewController.isEventOverKeyboardQuickSwitch(dragLayer, ev)) { + closeQuickSwitchView(true); + } + return false; + } + void onDestroy() { if (mQuickSwitchViewController != null) { mQuickSwitchViewController.onDestroy(); } } + @VisibleForTesting + boolean isShownFromTaskbar() { + return isShown() && mQuickSwitchViewController.wasOpenedFromTaskbar(); + } + + @VisibleForTesting + boolean isShown() { + return mQuickSwitchViewController != null + && !mQuickSwitchViewController.isCloseAnimationRunning(); + } + + @VisibleForTesting + List shownTaskIds() { + if (!isShown()) { + return Collections.emptyList(); + } + + return mTasks.stream().flatMap( + groupTask -> groupTask.getTasks().stream().map(task -> task.key.id)).toList(); + } + @Override public void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "KeyboardQuickSwitchController:"); @@ -214,17 +449,16 @@ public final class KeyboardQuickSwitchController implements pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null)); pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks); pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId); + pw.println(prefix + "\tmHasDesktopTask=" + mHasDesktopTask); + pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut); pw.println(prefix + "\tmTasks=["); for (GroupTask task : mTasks) { - Task task1 = task.task1; - Task task2 = task.task2; - ComponentName cn1 = task1.getTopComponent(); - ComponentName cn2 = task2 != null ? task2.getTopComponent() : null; - pw.println(prefix + "\t\tt1: (id=" + task1.key.id - + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)") - + " t2: (id=" + (task2 != null ? task2.key.id : "-1") - + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" - : "no package)")); + int count = 0; + for (Task t : task.getTasks()) { + ComponentName cn = t.getTopComponent(); + pw.println(prefix + "\t\tt" + (++count) + ": (id=" + t.key.id + + "; package=" + (cn != null ? cn.getPackageName() + ")" : "no package)")); + } } pw.println(prefix + "\t]"); @@ -235,24 +469,41 @@ public final class KeyboardQuickSwitchController implements class ControllerCallbacks { - int getTaskCount() { - return mTasks.size() + (mNumHiddenTasks == 0 ? 0 : 1); - } - @Nullable GroupTask getTaskAt(int index) { return index < 0 || index >= mTasks.size() ? null : mTasks.get(index); } void updateThumbnailInBackground(Task task, Consumer callback) { - mModel.getThumbnailCache().updateThumbnailInBackground(task, callback); + mModel.getThumbnailCache().getThumbnailInBackground(task, + thumbnailData -> { + task.thumbnail = thumbnailData; + callback.accept(thumbnailData); + }); } void updateIconInBackground(Task task, Consumer callback) { - mModel.getIconCache().updateIconInBackground(task, callback); + mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> { + task.icon = icon; + task.titleDescription = contentDescription; + task.title = title; + callback.accept(task); + }); + } + + void onCloseStarted() { + if (mOnClosed != null) { + mOnClosed.run(); + mOnClosed = null; + } } void onCloseComplete() { + if (ENABLE_TASKBAR_OVERFLOW.isTrue() && mOverlayContext != null) { + mOverlayContext.getDragLayer() + .removeTouchController(KeyboardQuickSwitchController.this); + } + mOverlayContext = null; mQuickSwitchViewController = null; } @@ -260,15 +511,23 @@ public final class KeyboardQuickSwitchController implements if (task == null) { return false; } - int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().taskId; - Task task2 = task.task2; + ActivityManager.RunningTaskInfo runningTaskInfo = + ActivityManagerWrapper.getInstance().getRunningTask(); + if (runningTaskInfo == null) { + return false; + } - return runningTaskId == task.task1.key.id - || (task2 != null && runningTaskId == task2.key.id); + int runningTaskId = runningTaskInfo.taskId; + return task.containsTask(runningTaskId); } boolean isFirstTaskRunning() { return isTaskRunning(getTaskAt(0)); } + + boolean isAspectRatioSquare() { + return mControllers != null && LayoutUtils.isAspectRatioSquare( + mControllers.taskbarActivityContext.getDeviceProfile().getDeviceProperties().getAspectRatio()); + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java index c564baab50..bcaca16278 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java @@ -39,34 +39,37 @@ import com.android.launcher3.util.Preconditions; import com.android.quickstep.util.BorderAnimator; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; - -import java.util.function.Consumer; +import com.android.wm.shell.shared.TypefaceUtils; +import com.android.wm.shell.shared.TypefaceUtils.FontFamily; +import com.android.wm.shell.shared.split.SplitBounds; import kotlin.Unit; +import java.util.function.Consumer; + /** * A view that displays a recent task during a keyboard quick switch. */ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { private static final float THUMBNAIL_BLUR_RADIUS = 1f; + private static final int INVALID_BORDER_RADIUS = -1; - @ColorInt - private final int mBorderColor; + @ColorInt private final int mBorderColor; + @ColorInt private final int mBorderRadius; - @Nullable - private BorderAnimator mBorderAnimator; + @Nullable private BorderAnimator mBorderAnimator; - @Nullable - private ImageView mThumbnailView1; - @Nullable - private ImageView mThumbnailView2; - @Nullable - private ImageView mIcon1; - @Nullable - private ImageView mIcon2; - @Nullable - private View mContent; + @Nullable private ImageView mThumbnailView1; + @Nullable private ImageView mThumbnailView2; + @Nullable private ImageView mIcon1; + @Nullable private ImageView mIcon2; + @Nullable private View mContent; + + // Describe the task position in the parent container. Used to add information about the task's + // position in a task list to the task view's content description. + private int mIndexInParent = -1; + private int mTotalTasksInParent = -1; public KeyboardQuickSwitchTaskView(@NonNull Context context) { this(context, null); @@ -94,6 +97,8 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { mBorderColor = ta.getColor( R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR); + mBorderRadius = ta.getDimensionPixelSize( + R.styleable.TaskView_focusBorderRadius, INVALID_BORDER_RADIUS); ta.recycle(); } @@ -106,14 +111,27 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { mIcon2 = findViewById(R.id.icon_2); mContent = findViewById(R.id.content); - Resources resources = mContext.getResources(); - Preconditions.assertNotNull(mContent); + + TypefaceUtils.setTypeface( + mContent.findViewById(R.id.large_text), + FontFamily.GSF_HEADLINE_LARGE_EMPHASIZED + ); + TypefaceUtils.setTypeface( + mContent.findViewById(R.id.small_text), + FontFamily.GSF_LABEL_LARGE + ); + + Resources resources = mContext.getResources(); mBorderAnimator = BorderAnimator.createScalingBorderAnimator( - /* borderRadiusPx= */ resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_task_view_radius), + /* borderRadiusPx= */ mBorderRadius != INVALID_BORDER_RADIUS + ? mBorderRadius + : resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_task_view_radius), /* borderWidthPx= */ resources.getDimensionPixelSize( R.dimen.keyboard_quick_switch_border_width), + /* borderStrokePx= */ resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_border_stroke), /* boundsBuilder= */ bounds -> { bounds.set(0, 0, getWidth(), getHeight()); return Unit.INSTANCE; @@ -144,36 +162,105 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction); applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction); + // Update content description, even in cases task icons, and content descriptions need to be + // loaded asynchronously to ensure that the task has non empty description (assuming task + // position information was set), as KeyboardQuickSwitch view may request accessibility + // focus to be moved to the task when the quick switch UI gets shown. The description will + // be updated once the task metadata has been loaded - the delay should be very short, and + // the content description when task titles are not available still gives some useful + // information to the user (the task's position in the list). + updateContentDesctiptionForTasks(task1, task2); + if (iconUpdateFunction == null) { applyIcon(mIcon1, task1); applyIcon(mIcon2, task2); - setContentDescription(task2 == null - ? task1.titleDescription - : getContext().getString( - R.string.quick_switch_split_task, - task1.titleDescription, - task2.titleDescription)); return; } + iconUpdateFunction.updateIconInBackground(task1, t -> { applyIcon(mIcon1, task1); if (task2 != null) { return; } - setContentDescription(task1.titleDescription); + updateContentDesctiptionForTasks(task1, null); }); + if (task2 == null) { return; } iconUpdateFunction.updateIconInBackground(task2, t -> { applyIcon(mIcon2, task2); - setContentDescription(getContext().getString( - R.string.quick_switch_split_task, - task1.titleDescription, - task2.titleDescription)); + updateContentDesctiptionForTasks(task1, task2); }); } + /** + * Initializes information about the task's position within the parent container context - used + * to add position information to the view's content description. + * Should be called before associating the view with tasks. + * + * @param index The view's 0-based index within the parent task container. + * @param totalTasks The total number of tasks in the parent task container. + */ + protected void setPositionInformation(int index, int totalTasks) { + mIndexInParent = index; + mTotalTasksInParent = totalTasks; + } + + protected void setThumbnailsForSplitTasks( + @NonNull Task task1, + @Nullable Task task2, + @Nullable ThumbnailUpdateFunction thumbnailUpdateFunction, + @Nullable IconUpdateFunction iconUpdateFunction, + @Nullable SplitBounds splitBounds) { + setThumbnails(task1, task2, thumbnailUpdateFunction, iconUpdateFunction); + + if (splitBounds == null) { + return; + } + + + final boolean isLeftRightSplit = !splitBounds.appsStackedVertically; + final float leftOrTopTaskPercent = splitBounds.getLeftTopTaskPercent(); + + ConstraintLayout.LayoutParams leftTopParams = (ConstraintLayout.LayoutParams) + mThumbnailView1.getLayoutParams(); + ConstraintLayout.LayoutParams rightBottomParams = (ConstraintLayout.LayoutParams) + mThumbnailView2.getLayoutParams(); + + if (isLeftRightSplit) { + // Set thumbnail view ratio in left right split mode. + leftTopParams.width = 0; // Set width to 0dp, so it uses the constraint dimension ratio. + leftTopParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT; + leftTopParams.matchConstraintPercentWidth = leftOrTopTaskPercent; + leftTopParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID; + leftTopParams.rightToLeft = R.id.thumbnail_2; + mThumbnailView1.setLayoutParams(leftTopParams); + + rightBottomParams.width = 0; + rightBottomParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT; + rightBottomParams.matchConstraintPercentWidth = 1 - leftOrTopTaskPercent; + rightBottomParams.leftToRight = R.id.thumbnail_1; + rightBottomParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID; + mThumbnailView2.setLayoutParams(rightBottomParams); + } else { + // Set thumbnail view ratio in top bottom split mode. + leftTopParams.height = 0; + leftTopParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT; + leftTopParams.matchConstraintPercentHeight = leftOrTopTaskPercent; + leftTopParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; + leftTopParams.bottomToTop = R.id.thumbnail_2; + mThumbnailView1.setLayoutParams(leftTopParams); + + rightBottomParams.height = 0; + rightBottomParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT; + rightBottomParams.matchConstraintPercentHeight = 1 - leftOrTopTaskPercent; + rightBottomParams.topToBottom = R.id.thumbnail_1; + rightBottomParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; + mThumbnailView2.setLayoutParams(rightBottomParams); + } + } + private void applyThumbnail( @Nullable ImageView thumbnailView, @Nullable Task task, @@ -185,8 +272,8 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { applyThumbnail(thumbnailView, task.colorBackground, task.thumbnail); return; } - updateFunction.updateThumbnailInBackground(task, - thumbnailData -> applyThumbnail(thumbnailView, task.colorBackground, thumbnailData)); + updateFunction.updateThumbnailInBackground(task, thumbnailData -> + applyThumbnail(thumbnailView, task.colorBackground, thumbnailData)); } private void applyThumbnail( @@ -218,6 +305,28 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { constantState.newDrawable(getResources(), getContext().getTheme())); } + /** + * Updates the task view's content description to reflect tasks represented by the view. + */ + private void updateContentDesctiptionForTasks(@NonNull Task task1, @Nullable Task task2) { + String tasksDescription = task1.titleDescription == null || task2 == null + ? task1.titleDescription + : getContext().getString( + R.string.quick_switch_split_task, + task1.titleDescription, + task2.titleDescription); + if (mIndexInParent < 0) { + setContentDescription(tasksDescription); + return; + } + + setContentDescription( + getContext().getString(R.string.quick_switch_task_with_position_in_parent, + tasksDescription != null ? tasksDescription : "", + mIndexInParent + 1, + mTotalTasksInParent)); + } + protected interface ThumbnailUpdateFunction { void updateThumbnailInBackground(Task task, Consumer callback); diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java index 5b7512a4cc..7a17d6d2b9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java @@ -17,7 +17,8 @@ package com.android.launcher3.taskbar; import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID; -import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS; +import static com.android.launcher3.taskbar.TaskbarDesktopExperienceFlags.enableAltTabKqsFlatenning; +import static com.android.launcher3.taskbar.TaskbarDesktopExperienceFlags.enableAltTabKqsOnConnectedDisplays; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -25,51 +26,72 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Outline; +import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.RectF; import android.icu.text.MessageFormat; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.widget.HorizontalScrollView; -import android.widget.ImageView; +import android.widget.ImageButton; import android.widget.TextView; +import android.window.OnBackInvokedDispatcher; +import android.window.WindowOnBackInvokedDispatcher; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.content.res.ResourcesCompat; import com.android.app.animation.Interpolators; +import com.android.internal.jank.Cuj; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.icons.GraphicsUtils; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.Themes; +import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SingleTask; +import com.android.quickstep.util.SplitTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.system.InteractionJankMonitorWrapper; +import com.android.wm.shell.shared.TypefaceUtils; +import com.android.wm.shell.shared.TypefaceUtils.FontFamily; import java.util.HashMap; import java.util.List; import java.util.Locale; /** - * View that allows quick switching between recent tasks through keyboard - * alt-tab and alt-shift-tab - * commands. + * View that allows quick switching between recent tasks. + * + * Can be access via: + * - keyboard alt-tab + * - alt-shift-tab + * - taskbar overflow button */ public class KeyboardQuickSwitchView extends ConstraintLayout { private static final long OUTLINE_ANIMATION_DURATION_MS = 333; private static final float OUTLINE_START_HEIGHT_FACTOR = 0.45f; private static final float OUTLINE_START_RADIUS_FACTOR = 0.25f; - private static final Interpolator OPEN_OUTLINE_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE; - private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR = Interpolators.EMPHASIZED_ACCELERATE; + private static final Interpolator OPEN_OUTLINE_INTERPOLATOR = + Interpolators.EMPHASIZED_DECELERATE; + private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR = + Interpolators.EMPHASIZED_ACCELERATE; private static final long ALPHA_ANIMATION_DURATION_MS = 83; private static final long ALPHA_ANIMATION_START_DELAY_MS = 67; @@ -79,12 +101,19 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { private static final float CONTENT_START_TRANSLATION_X_DP = 32; private static final float CONTENT_START_TRANSLATION_Y_DP = 40; private static final Interpolator OPEN_TRANSLATION_X_INTERPOLATOR = Interpolators.EMPHASIZED; - private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE; - private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR = Interpolators.EMPHASIZED_ACCELERATE; + private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR = + Interpolators.EMPHASIZED_DECELERATE; + private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR = + Interpolators.EMPHASIZED_ACCELERATE; private static final long CONTENT_ALPHA_ANIMATION_DURATION_MS = 83; private static final long CONTENT_ALPHA_ANIMATION_START_DELAY_MS = 83; + private static final int DARK_THEME_STROKE_ALPHA = 51; + private static final int LIGHT_THEME_STROKE_ALPHA = 41; + private static final int DARK_THEME_SHADOW_ALPHA = 51; + private static final int LIGHT_THEME_SHADOW_ALPHA = 25; + private final AnimatedFloat mOutlineAnimationProgress = new AnimatedFloat( this::invalidateOutline); @@ -93,15 +122,33 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { private HorizontalScrollView mScrollView; private ConstraintLayout mContent; - private int mTaskViewWidth; - private int mTaskViewHeight; + private boolean mSupportsScrollArrows = false; + private ImageButton mStartScrollArrow; + private ImageButton mEndScrollArrow; + + private int mTaskViewBorderWidth; + private int mTaskViewRadius; private int mSpacing; + private int mSmallSpacing; private int mOutlineRadius; private boolean mIsRtl; + // Used to paint a background with a shadow. + private final Paint mBackgroundPaint = new Paint(); + private float mBackgroundShadowBlur; + private float mBackgroundShadowDistance; + private final Paint mStrokePaint = new Paint(); + private final RectF mLastBackgroundRect = new RectF(); + + + private int mOverviewTaskIndex = -1; + private int mDesktopTaskIndex = -1; + @Nullable private AnimatorSet mOpenAnimation; + private boolean mIsBackCallbackRegistered = false; + @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks; @@ -124,26 +171,111 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { super(context, attrs, defStyleAttr, defStyleRes); } + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mViewCallbacks != null) { + mViewCallbacks.onViewDetchedFromWindow(); + } + } + @Override protected void onFinishInflate() { super.onFinishInflate(); mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane); mScrollView = findViewById(R.id.scroll_view); mContent = findViewById(R.id.content); + mStartScrollArrow = findViewById(R.id.scroll_button_start); + mEndScrollArrow = findViewById(R.id.scroll_button_end); + + setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); Resources resources = getResources(); - mTaskViewWidth = resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_taskview_width); - mTaskViewHeight = resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_taskview_height); mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing); + mSmallSpacing = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_view_small_spacing); mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius); + mTaskViewBorderWidth = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_border_width); + mTaskViewRadius = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_task_view_radius); + mIsRtl = Utilities.isRtl(resources); + + mBackgroundPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + mBackgroundPaint.setStyle(Paint.Style.FILL); + mBackgroundPaint.setColor( + Themes.getAttrColor(getContext(), R.attr.overviewScrimColorFallback)); + mBackgroundShadowBlur = resources.getDimension(R.dimen.transient_taskbar_shadow_blur); + mBackgroundShadowDistance = resources.getDimension( + R.dimen.transient_taskbar_key_shadow_distance); + + + mStrokePaint.setFlags(Paint.ANTI_ALIAS_FLAG); + mStrokePaint.setStyle(Paint.Style.STROKE); + mStrokePaint.setStrokeWidth( + getResources().getDimension(R.dimen.transient_taskbar_stroke_width)); + mStrokePaint.setColor( + getResources().getColor(R.color.taskbar_stroke, getContext().getTheme())); + + TypefaceUtils.setTypeface( + mNoRecentItemsPane.findViewById(R.id.no_recent_items_text), + FontFamily.GSF_LABEL_LARGE); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + boolean isDarkTheme = Utilities.isDarkTheme(getContext()); + mStrokePaint.setAlpha(isDarkTheme ? DARK_THEME_STROKE_ALPHA : LIGHT_THEME_STROKE_ALPHA); + + // Draw shadow. + mBackgroundPaint.setShadowLayer( + mBackgroundShadowBlur, + 0, + mBackgroundShadowDistance, + GraphicsUtils.setColorAlphaBound(Color.BLACK, + isDarkTheme ? DARK_THEME_SHADOW_ALPHA : LIGHT_THEME_SHADOW_ALPHA)); + mLastBackgroundRect.set(0, 0, getWidth(), getHeight()); + + canvas.drawRoundRect(mLastBackgroundRect, mOutlineRadius, mOutlineRadius, mBackgroundPaint); + canvas.drawRoundRect(mLastBackgroundRect, mOutlineRadius, mOutlineRadius, mStrokePaint); + } + + private void registerOnBackInvokedCallback() { + OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); + + if (isOnBackInvokedCallbackEnabled(dispatcher) + && !mIsBackCallbackRegistered) { + dispatcher.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_OVERLAY, mViewCallbacks.onBackInvokedCallback); + mIsBackCallbackRegistered = true; + } + } + + private void unregisterOnBackInvokedCallback() { + OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); + + if (isOnBackInvokedCallbackEnabled(dispatcher) + && mIsBackCallbackRegistered) { + dispatcher.unregisterOnBackInvokedCallback( + mViewCallbacks.onBackInvokedCallback); + mIsBackCallbackRegistered = false; + } + } + + private boolean isOnBackInvokedCallbackEnabled(OnBackInvokedDispatcher dispatcher) { + return dispatcher instanceof WindowOnBackInvokedDispatcher + && ((WindowOnBackInvokedDispatcher) dispatcher).isOnBackInvokedCallbackEnabled() + && mViewCallbacks != null; } private KeyboardQuickSwitchTaskView createAndAddTaskView( int index, boolean isFinalView, + boolean useSmallStartSpacing, @LayoutRes int resId, @NonNull LayoutInflater layoutInflater, @Nullable View previousView) { @@ -152,7 +284,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { taskView.setId(View.generateViewId()); taskView.setOnClickListener(v -> mViewCallbacks.launchTaskAt(index)); - LayoutParams lp = new LayoutParams(mTaskViewWidth, mTaskViewHeight); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); // Create a left-to-right ordering of views (or right-to-left in RTL locales) if (previousView != null) { lp.startToEnd = previousView.getId(); @@ -162,10 +294,9 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { lp.topToTop = PARENT_ID; lp.bottomToBottom = PARENT_ID; // Add spacing between views - lp.setMarginStart(mSpacing); + lp.setMarginStart(useSmallStartSpacing ? mSmallSpacing : mSpacing); if (isFinalView) { - // Add spacing to the end of the final view so that scrolling ends with some - // padding. + // Add spacing to the end of the final view so that scrolling ends with some padding. lp.endToEnd = PARENT_ID; lp.setMarginEnd(mSpacing); lp.horizontalBias = 1f; @@ -182,67 +313,97 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { int numHiddenTasks, boolean updateTasks, int currentFocusIndexOverride, - @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) { + @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks, + boolean useDesktopTaskView) { + mContent.removeAllViews(); + mViewCallbacks = viewCallbacks; Resources resources = context.getResources(); Resources.Theme theme = context.getTheme(); View previousTaskView = null; LayoutInflater layoutInflater = LayoutInflater.from(context); - int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size()); + int tasksToDisplay = groupTasks.size(); for (int i = 0; i < tasksToDisplay; i++) { GroupTask groupTask = groupTasks.get(i); KeyboardQuickSwitchTaskView currentTaskView = createAndAddTaskView( i, - /* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0, - groupTask instanceof DesktopTask - ? R.layout.keyboard_quick_switch_textonly_taskview + /* isFinalView= */ i == tasksToDisplay - 1 + && numHiddenTasks == 0 && !useDesktopTaskView, + /* useSmallStartSpacing= */ false, + mViewCallbacks.isAspectRatioSquare() + ? R.layout.keyboard_quick_switch_taskview_square : R.layout.keyboard_quick_switch_taskview, layoutInflater, previousTaskView); - if (groupTask instanceof DesktopTask desktopTask) { - HashMap args = new HashMap<>(); - args.put("count", desktopTask.tasks.size()); - - currentTaskView.findViewById(R.id.icon).setImageDrawable( - ResourcesCompat.getDrawable(resources, R.drawable.ic_desktop, theme)); - currentTaskView.findViewById(R.id.text).setText(new MessageFormat( - resources.getString(R.string.quick_switch_desktop), - Locale.getDefault()).format(args)); + Task task1; + Task task2; + if (groupTask instanceof SplitTask splitTask) { + task1 = splitTask.getTopLeftTask(); + task2 = splitTask.getBottomRightTask(); + } else if (groupTask instanceof SingleTask singleTask) { + task1 = singleTask.getTask(); + task2 = null; + } else if (enableAltTabKqsFlatenning.isTrue() + && groupTask instanceof DesktopTask desktopTask) { + task1 = desktopTask.getTasks().get(0); + task2 = null; } else { - currentTaskView.setThumbnails( - groupTask.task1, - groupTask.task2, - updateTasks ? mViewCallbacks::updateThumbnailInBackground : null, - updateTasks ? mViewCallbacks::updateIconInBackground : null); + continue; } + + currentTaskView.setPositionInformation(i, tasksToDisplay); + currentTaskView.setThumbnailsForSplitTasks( + task1, + task2, + updateTasks ? mViewCallbacks::updateThumbnailInBackground : null, + updateTasks ? mViewCallbacks::updateIconInBackground : null, + groupTask instanceof SplitTask splitTask ? splitTask.getSplitBounds() : null); + previousTaskView = currentTaskView; } - if (numHiddenTasks > 0) { HashMap args = new HashMap<>(); args.put("count", numHiddenTasks); + mOverviewTaskIndex = getTaskCount(); View overviewButton = createAndAddTaskView( - MAX_TASKS, - /* isFinalView= */ true, - R.layout.keyboard_quick_switch_textonly_taskview, + mOverviewTaskIndex, + /* isFinalView= */ !useDesktopTaskView, + /* useSmallStartSpacing= */ false, + R.layout.keyboard_quick_switch_overview_taskview, layoutInflater, previousTaskView); - overviewButton.findViewById(R.id.icon).setImageDrawable( - ResourcesCompat.getDrawable(resources, R.drawable.view_carousel, theme)); - overviewButton.findViewById(R.id.text).setText(new MessageFormat( + overviewButton.findViewById(R.id.large_text).setText( + String.format(Locale.getDefault(), "%d", numHiddenTasks)); + overviewButton.findViewById(R.id.small_text).setText(new MessageFormat( resources.getString(R.string.quick_switch_overflow), Locale.getDefault()).format(args)); + + previousTaskView = overviewButton; } - mDisplayingRecentTasks = !groupTasks.isEmpty(); + if (useDesktopTaskView) { + mDesktopTaskIndex = getTaskCount(); + View desktopButton = createAndAddTaskView( + mDesktopTaskIndex, + /* isFinalView= */ true, + /* useSmallStartSpacing= */ numHiddenTasks > 0, + R.layout.keyboard_quick_switch_desktop_taskview, + layoutInflater, + previousTaskView); + + desktopButton.findViewById(R.id.small_text).setText( + resources.getString(R.string.quick_switch_desktop)); + } + mDisplayingRecentTasks = !groupTasks.isEmpty() || useDesktopTaskView; getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { + registerOnBackInvokedCallback(); animateOpen(currentFocusIndexOverride); getViewTreeObserver().removeOnGlobalLayoutListener(this); @@ -250,6 +411,123 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { }); } + + void enableScrollArrowSupport() { + if (mSupportsScrollArrows) { + return; + } + mSupportsScrollArrows = true; + + if (mIsRtl) { + mStartScrollArrow.setContentDescription( + getResources().getString(R.string.quick_switch_scroll_arrow_right)); + mEndScrollArrow.setContentDescription( + getResources().getString(R.string.quick_switch_scroll_arrow_left)); + } + + + mStartScrollArrow.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mIsRtl) { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(mScrollView.getWidth(), 0); + }); + } else { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0); + }); + } + } + }); + + mEndScrollArrow.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mIsRtl) { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0); + }); + } else { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(mScrollView.getWidth(), 0); + }); + } + } + }); + + // Add listeners to disable arrow buttons when the scroll view cannot be further scrolled in + // the associated direction. + mScrollView.setOnScrollChangeListener(new OnScrollChangeListener() { + @Override + public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, + int oldScrollY) { + updateArrowButtonsEnabledState(); + } + }); + + // Update scroll view outline to clip its contents with rounded corners. + mScrollView.setClipToOutline(true); + mScrollView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + int spacingWithoutBorder = mSpacing - mTaskViewBorderWidth; + outline.setRoundRect(spacingWithoutBorder, + spacingWithoutBorder, view.getWidth() - spacingWithoutBorder, + view.getHeight() - spacingWithoutBorder, + mTaskViewRadius); + } + }); + } + + private void updateArrowButtonsEnabledState() { + if (!mDisplayingRecentTasks) { + return; + } + + int scrollX = mScrollView.getScrollX(); + if (mIsRtl) { + mEndScrollArrow.setEnabled(scrollX > 0); + mStartScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth()); + } else { + mStartScrollArrow.setEnabled(scrollX > 0); + mEndScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth()); + } + } + + int getOverviewTaskIndex() { + return mOverviewTaskIndex; + } + + int getDesktopTaskIndex() { + return mDesktopTaskIndex; + } + + void resetViewCallbacks() { + // Unregister the back invoked callback after the view is closed and before the + // mViewCallbacks is reset. + unregisterOnBackInvokedCallback(); + if (enableAltTabKqsOnConnectedDisplays.isTrue()) { + SystemUiProxy.INSTANCE.get(getContext()).getFocusState().removeListener(mViewCallbacks); + } + mViewCallbacks = null; + } + + private void animateDisplayedContentForClose(View view, AnimatorSet animator) { + Animator translationYAnimation = ObjectAnimator.ofFloat( + view, + TRANSLATION_Y, + 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP)); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR); + animator.play(translationYAnimation); + + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 1f, 0f); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + animator.play(contentAlphaAnimation); + + } + protected Animator getCloseAnimation() { AnimatorSet closeAnimation = new AnimatorSet(); @@ -264,17 +542,11 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { closeAnimation.play(alphaAnimation); View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane; - Animator translationYAnimation = ObjectAnimator.ofFloat( - displayedContent, - TRANSLATION_Y, - 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP)); - translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); - translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR); - closeAnimation.play(translationYAnimation); - - Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 1f, 0f); - contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); - closeAnimation.play(contentAlphaAnimation); + animateDisplayedContentForClose(displayedContent, closeAnimation); + if (mSupportsScrollArrows) { + animateDisplayedContentForClose(mStartScrollArrow, closeAnimation); + animateDisplayedContentForClose(mEndScrollArrow, closeAnimation); + } closeAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -289,12 +561,42 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { return closeAnimation; } - private void animateOpen(int currentFocusIndexOverride) { + private void animateDisplayedContentForOpen(View view, AnimatorSet animator) { + Animator translationXAnimation = ObjectAnimator.ofFloat( + view, + TRANSLATION_X, + -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0); + translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS); + translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR); + animator.play(translationXAnimation); + + Animator translationYAnimation = ObjectAnimator.ofFloat( + view, + TRANSLATION_Y, + -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR); + animator.play(translationYAnimation); + + view.setAlpha(0.0f); + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 0f, + 1f); + contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + animator.play(contentAlphaAnimation); + } + + protected void animateOpen(int currentFocusIndexOverride) { if (mOpenAnimation != null) { - // Restart animation since currentFocusIndexOverride can change the initial - // scroll. + // Restart animation since currentFocusIndexOverride can change the initial scroll. mOpenAnimation.cancel(); } + + // Reset the alpha for the case where the KQS view is opened before. + setAlpha(0); + mScrollView.setAlpha(0); + mNoRecentItemsPane.setAlpha(0); + mOpenAnimation = new AnimatorSet(); Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f); @@ -306,32 +608,23 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { mOpenAnimation.play(alphaAnimation); View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane; - Animator translationXAnimation = ObjectAnimator.ofFloat( - displayedContent, - TRANSLATION_X, - -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0); - translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS); - translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR); - mOpenAnimation.play(translationXAnimation); + animateDisplayedContentForOpen(displayedContent, mOpenAnimation); + if (mSupportsScrollArrows) { + animateDisplayedContentForOpen(mStartScrollArrow, mOpenAnimation); + animateDisplayedContentForOpen(mEndScrollArrow, mOpenAnimation); + } - Animator translationYAnimation = ObjectAnimator.ofFloat( - displayedContent, - TRANSLATION_Y, - -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0); - translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); - translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR); - mOpenAnimation.play(translationYAnimation); - - Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 0f, 1f); - contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS); - contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); - mOpenAnimation.play(contentAlphaAnimation); ViewOutlineProvider outlineProvider = getOutlineProvider(); + int defaultFocusedTaskIndex = Math.min( + getTaskCount() - 1, + currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride); mOpenAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); + InteractionJankMonitorWrapper.begin( + KeyboardQuickSwitchView.this, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN); setClipToPadding(false); setOutlineProvider(new ViewOutlineProvider() { @Override @@ -358,12 +651,41 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { OPEN_OUTLINE_INTERPOLATOR)); } }); - animateFocusMove(-1, Math.min( - mContent.getChildCount() - 1, - currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride)); + + if (mSupportsScrollArrows) { + mScrollView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mScrollView.getWidth() == 0) { + return; + } + + if (mContent.getWidth() > mScrollView.getWidth()) { + mStartScrollArrow.setVisibility(VISIBLE); + mEndScrollArrow.setVisibility(VISIBLE); + updateArrowButtonsEnabledState(); + } + mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener( + this); + } + }); + } + + animateFocusMove(-1, defaultFocusedTaskIndex); displayedContent.setVisibility(VISIBLE); setVisibility(VISIBLE); requestFocus(); + if (enableAltTabKqsOnConnectedDisplays.isTrue()) { + SystemUiProxy.INSTANCE.get(getContext()).getFocusState().addListener( + mViewCallbacks); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN); } @Override @@ -373,6 +695,12 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { setOutlineProvider(outlineProvider); invalidateOutline(); mOpenAnimation = null; + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN); + + View focusedTask = getTaskAt(defaultFocusedTaskIndex); + if (focusedTask != null) { + focusedTask.requestAccessibilityFocus(); + } } }); @@ -404,8 +732,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { int firstVisibleTaskIndex = toIndex == 0 ? toIndex : getTaskAt(toIndex - 1) == null - ? toIndex - : toIndex - 1; + ? toIndex : toIndex - 1; // Scroll so that the previous task view is truncated as a visual hint that // there are more tasks initializeScroll( @@ -536,8 +863,9 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { } private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) { - boolean isTargetTruncated = targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth() - || Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX(); + boolean isTargetTruncated = + targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth() + || Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX(); return isTargetTruncated && !shouldTruncateTarget; } @@ -559,8 +887,11 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { @Nullable protected KeyboardQuickSwitchTaskView getTaskAt(int index) { - return !mDisplayingRecentTasks || index < 0 || index >= mContent.getChildCount() - ? null - : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index); + return !mDisplayingRecentTasks || index < 0 || index >= getTaskCount() + ? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index); + } + + public int getTaskCount() { + return mContent.getChildCount(); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java index d6ee92f195..5dc4fd142a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java @@ -15,30 +15,47 @@ */ package com.android.launcher3.taskbar; -import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_OVERFLOW; +import static com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType.UNMINIMIZE; +import static com.android.launcher3.taskbar.TaskbarDesktopExperienceFlags.enableAltTabKqsFlatenning; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import android.animation.Animator; -import android.app.ActivityOptions; +import android.animation.AnimatorListenerAdapter; +import android.content.res.Resources; +import android.view.Gravity; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.animation.AnimationUtils; +import android.window.OnBackInvokedCallback; import android.window.RemoteTransition; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.jank.Cuj; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorListeners; +import com.android.launcher3.desktop.DesktopAppLaunchTransition; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.FocusState; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SingleTask; import com.android.quickstep.util.SlideInRemoteTransition; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.QuickStepContract; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason; import java.io.PrintWriter; import java.util.List; @@ -61,6 +78,10 @@ public class KeyboardQuickSwitchViewController { private int mCurrentFocusIndex = -1; private boolean mOnDesktop; + private boolean mWasDesktopTaskFilteredOut; + private boolean mWasOpenedFromTaskbar; + + private boolean mDetachingFromWindow = false; protected KeyboardQuickSwitchViewController( @NonNull TaskbarControllers controllers, @@ -77,14 +98,36 @@ public class KeyboardQuickSwitchViewController { return mCurrentFocusIndex; } + protected boolean wasOpenedFromTaskbar() { + return mWasOpenedFromTaskbar; + } + protected void openQuickSwitchView( @NonNull List tasks, int numHiddenTasks, boolean updateTasks, int currentFocusIndexOverride, - boolean onDesktop) { + boolean onDesktop, + boolean hasDesktopTask, + boolean wasDesktopTaskFilteredOut, + boolean wasOpenedFromTaskbar) { + final boolean isTransientTaskBar = mControllers.taskbarActivityContext.isTransientTaskbar(); + positionView(wasOpenedFromTaskbar, isTransientTaskBar); + + // Keep the taskbar unstashed if the KQS is opened. + if (wasOpenedFromTaskbar && isTransientTaskBar) { + mControllers.taskbarStashController.updateTaskbarTimeout(/* isAutohideSuspended= */ + true); + } + mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView); mOnDesktop = onDesktop; + mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut; + mWasOpenedFromTaskbar = wasOpenedFromTaskbar; + + if (ENABLE_TASKBAR_OVERFLOW.isTrue() && wasOpenedFromTaskbar) { + mKeyboardQuickSwitchView.enableScrollArrowSupport(); + } mKeyboardQuickSwitchView.applyLoadPlan( mOverlayContext, @@ -92,7 +135,67 @@ public class KeyboardQuickSwitchViewController { numHiddenTasks, updateTasks, currentFocusIndexOverride, - mViewCallbacks); + mViewCallbacks, + /* useDesktopTaskView= */ !onDesktop && hasDesktopTask); + } + + protected void updateQuickSwitchView( + @NonNull List tasks, + int numHiddenTasks, + int currentFocusIndexOverride, + boolean hasDesktopTask, + boolean wasDesktopTaskFilteredOut) { + mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut; + mKeyboardQuickSwitchView.applyLoadPlan( + mOverlayContext, + tasks, + numHiddenTasks, + /* updateTasks= */ true, + currentFocusIndexOverride, + mViewCallbacks, + /* useDesktopTaskView= */ !mOnDesktop && hasDesktopTask); + } + + protected void positionView(boolean wasOpenedFromTaskbar, boolean isTransientTaskbar) { + if (!wasOpenedFromTaskbar) { + // Keep the default positioning. + return; + } + + BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams( + mKeyboardQuickSwitchView.getLayoutParams()); + final Resources resources = mKeyboardQuickSwitchView.getResources(); + final int marginHorizontal = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_margin_ends); + + final DeviceProfile dp = mControllers.taskbarActivityContext.getDeviceProfile(); + // Calculate the additional margin space that the KQS should move up for the transient + // taskbar. The value of spaceForTaskbar is the distance between the bottom of the KQS + // view with 0 bottom margin to the top of the transient taskbar view. + final int spaceForTaskbar = isTransientTaskbar ? dp.getTaskbarProfile().getHeight() + + dp.getTaskbarProfile().getBottomMargin() + - dp.getTaskbarProfile().getStashedTaskbarHeight() : 0; + final int marginBottom = spaceForTaskbar + resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_margin_bottom); + + lp.setMargins(marginHorizontal, 0, marginHorizontal, marginBottom); + lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT; + lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + mKeyboardQuickSwitchView.setLayoutParams(lp); + } + + protected void updateLayoutForSurface(boolean updateLayoutFromTaskbar, + int currentFocusIndexOverride) { + BaseDragLayer.LayoutParams lp = + (BaseDragLayer.LayoutParams) mKeyboardQuickSwitchView.getLayoutParams(); + + if (updateLayoutFromTaskbar) { + lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT; + } else { + lp.width = BaseDragLayer.LayoutParams.MATCH_PARENT; + } + + mKeyboardQuickSwitchView.animateOpen(currentFocusIndexOverride); } boolean isCloseAnimationRunning() { @@ -101,18 +204,29 @@ public class KeyboardQuickSwitchViewController { protected void closeQuickSwitchView(boolean animate) { if (isCloseAnimationRunning()) { - // Let currently-running animation finish. if (!animate) { mCloseAnimation.end(); } + // Let currently-running animation finish. return; } + mControllerCallbacks.onCloseStarted(); if (!animate) { + InteractionJankMonitorWrapper.begin( + mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE); onCloseComplete(); return; } mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation(); + mCloseAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + InteractionJankMonitorWrapper.begin( + mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE); + } + }); mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete)); mCloseAnimation.start(); } @@ -131,7 +245,7 @@ public class KeyboardQuickSwitchViewController { } // If the user quick switches too quickly, updateCurrentFocusIndex might not have run. return launchTaskAt(mControllerCallbacks.isFirstTaskRunning() - && mControllerCallbacks.getTaskCount() > 1 ? 1 : 0); + && mKeyboardQuickSwitchView.getTaskCount() > 1 ? 1 : 0); } private int launchTaskAt(int index) { @@ -139,54 +253,116 @@ public class KeyboardQuickSwitchViewController { // Ignore taps on task views and alt key unpresses while the close animation is running. return -1; } + if (index == mKeyboardQuickSwitchView.getOverviewTaskIndex()) { + // If there is a desktop task view, then we should account for it when focusing the + // first hidden non-desktop task view in recents view + return mOnDesktop ? 1 : (mWasDesktopTaskFilteredOut ? index + 1 : index); + } + TaskbarActivityContext context = mControllers.taskbarActivityContext; + final RemoteTransition slideInTransition = new RemoteTransition(new SlideInRemoteTransition( + Utilities.isRtl(mControllers.taskbarActivityContext.getResources()), + context.getDeviceProfile().getOverviewProfile().getPageSpacing(), + QuickStepContract.getWindowCornerRadius(context), + AnimationUtils.loadInterpolator( + context, android.R.interpolator.fast_out_extra_slow_in)), + "SlideInTransition"); + SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get( + mKeyboardQuickSwitchView.getContext()); + if (index == mKeyboardQuickSwitchView.getDesktopTaskIndex()) { + UI_HELPER_EXECUTOR.execute(() -> + systemUiProxy + .showDesktopApps( + mKeyboardQuickSwitchView.getDisplay().getDisplayId(), + slideInTransition)); + return -1; + } // Even with a valid index, this can be null if the user tries to quick switch before the // views have been added in the KeyboardQuickSwitchView. GroupTask task = mControllerCallbacks.getTaskAt(index); if (task == null) { return mOnDesktop ? 1 : Math.max(0, index); } + + if (enableAltTabKqsFlatenning.isTrue() + && tryLaunchingCombinedTask(task, slideInTransition, systemUiProxy)) { + return -1; + } + + // TODO b/414410702: move this check to before tryLaunchingCombinedTask() call. if (mControllerCallbacks.isTaskRunning(task)) { // Ignore attempts to run the selected task if it is already running. return -1; } - TaskbarActivityContext context = mControllers.taskbarActivityContext; - RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition( - Utilities.isRtl(mControllers.taskbarActivityContext.getResources()), - context.getDeviceProfile().overviewPageSpacing, - QuickStepContract.getWindowCornerRadius(context), - AnimationUtils.loadInterpolator( - context, android.R.interpolator.fast_out_extra_slow_in)), - "SlideInTransition"); - if (task instanceof DesktopTask) { - UI_HELPER_EXECUTOR.execute(() -> - SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext()) - .showDesktopApps( - mKeyboardQuickSwitchView.getDisplay().getDisplayId(), - remoteTransition)); - } else if (mOnDesktop) { - UI_HELPER_EXECUTOR.execute(() -> - SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext()) - .showDesktopApp(task.task1.key.id)); - } else if (task.task2 == null) { - UI_HELPER_EXECUTOR.execute(() -> { - ActivityOptions activityOptions = mControllers.taskbarActivityContext - .makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options; - activityOptions.setRemoteTransition(remoteTransition); - - ActivityManagerWrapper.getInstance().startActivityFromRecents( - task.task1.key, activityOptions); - }); - } else { - mControllers.uiController.launchSplitTasks(task, remoteTransition); + RemoteTransition remoteTransition = slideInTransition; + boolean canUnminimizeDesktopTask = task instanceof SingleTask singleTask + && mControllers.taskbarActivityContext.canUnminimizeDesktopTask( + singleTask.getTask().key.id); + if (mOnDesktop && canUnminimizeDesktopTask) { + // This app is being unminimized - use our own transition runner. + remoteTransition = getUnminimizeTransition(); } + mControllers.taskbarActivityContext.handleGroupTaskLaunch( + task, + remoteTransition, + mOnDesktop, + DesktopTaskToFrontReason.ALT_TAB); return -1; } + private boolean tryLaunchingCombinedTask(GroupTask task, RemoteTransition slideInTransition, + SystemUiProxy systemUiProxy) { + TaskbarActivityContext context = mControllers.taskbarActivityContext; + int taskId = task.getTasks().getFirst().key.id; + + // All DesktopTasks, irrespective of whether desktop mode is active, are launched here as + // the class DesktopTask is used in a special way by KQS view for showing thumbnails of + // freeform tasks. + if (task instanceof DesktopTask desktopTask) { + boolean canUnminimizeDesktopTask = context.canUnminimizeDesktopTask(taskId); + UI_HELPER_EXECUTOR.execute(() -> { + if (!mOnDesktop) { + systemUiProxy.activateDesk(desktopTask.getDeskId(), slideInTransition); + } + + systemUiProxy.showDesktopApp(taskId, + canUnminimizeDesktopTask ? getUnminimizeTransition() : null, + DesktopTaskToFrontReason.ALT_TAB); + }); + return true; + } else if (mOnDesktop && task instanceof SingleTask) { + // Use the special API if user wants to switch to a fullscreen app while in desktop. + UI_HELPER_EXECUTOR.execute( + () -> systemUiProxy.moveToFullscreen(taskId, + DesktopModeTransitionSource.KEYBOARD_SHORTCUT, slideInTransition)); + return true; + } + + // For all other cases, let TaskbarActivityContext handle launching the task. + return false; + } + + private RemoteTransition getUnminimizeTransition() { + return new RemoteTransition( + new DesktopAppLaunchTransition( + mControllers.taskbarActivityContext, + UNMINIMIZE, + Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH, + MAIN_EXECUTOR + ), + "DesktopKeyboardQuickSwitchUnminimize"); + } + private void onCloseComplete() { mCloseAnimation = null; - mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView); + // Reset the view callbacks to prevent `onDetachedFromWindow` getting called in response to + // the `removeView(mKeyboardQuickSwitchView)` call. + mKeyboardQuickSwitchView.resetViewCallbacks(); + if (!mDetachingFromWindow) { + mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView); + } mControllerCallbacks.onCloseComplete(); + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE); } protected void onDestroy() { @@ -199,9 +375,20 @@ public class KeyboardQuickSwitchViewController { pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus()); pw.println(prefix + "\tisCloseAnimationRunning=" + isCloseAnimationRunning()); pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex); + pw.println(prefix + "\tmOnDesktop=" + mOnDesktop); + pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut); + pw.println(prefix + "\tmWasOpenedFromTaskbar=" + mWasOpenedFromTaskbar); } - class ViewCallbacks { + /** + * @return True if the MotionEvent is over the {@link KeyboardQuickSwitchView}. + */ + protected boolean isEventOverKeyboardQuickSwitch(TaskbarOverlayDragLayer dl, MotionEvent ev) { + return dl.isEventOverView(mKeyboardQuickSwitchView, ev); + } + + class ViewCallbacks implements FocusState.FocusChangeListener { + public final OnBackInvokedCallback onBackInvokedCallback = () -> closeQuickSwitchView(true); boolean onKeyUp(int keyCode, KeyEvent event, boolean isRTL, boolean allowTraversal) { if (keyCode != KeyEvent.KEYCODE_TAB @@ -226,7 +413,7 @@ public class KeyboardQuickSwitchViewController { boolean traverseBackwards = (keyCode == KeyEvent.KEYCODE_TAB && event.isShiftPressed()) || (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && isRTL) || (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !isRTL); - int taskCount = mControllerCallbacks.getTaskCount(); + int taskCount = mKeyboardQuickSwitchView.getTaskCount(); int toIndex = mCurrentFocusIndex == -1 // Focus the second-most recent app if possible ? (taskCount > 1 ? 1 : 0) @@ -261,5 +448,22 @@ public class KeyboardQuickSwitchViewController { void updateIconInBackground(Task task, Consumer callback) { mControllerCallbacks.updateIconInBackground(task, callback); } + + boolean isAspectRatioSquare() { + return mControllerCallbacks.isAspectRatioSquare(); + } + + void onViewDetchedFromWindow() { + mDetachingFromWindow = true; + closeQuickSwitchView(false); + mDetachingFromWindow = false; + } + + @Override + public void onFocusedDisplayChanged(int displayId) { + if (mControllers.taskbarActivityContext.getDisplayId() != displayId) { + closeQuickSwitchView(/* animate= */ true); + } + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 0270481571..e294f4ab9a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -15,12 +15,16 @@ */ package com.android.launcher3.taskbar; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; + +import static com.android.launcher3.Flags.syncAppLaunchWithTaskbarStash; +import static com.android.launcher3.QuickstepTransitionManager.TASKBAR_TO_APP_DURATION; import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION; +import static com.android.launcher3.QuickstepTransitionManager.getTaskbarToHomeDuration; import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE; import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES; import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; -import static com.android.window.flags2.Flags.enableDesktopWindowingWallpaperActivity; +import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IGNORE_IN_APP; import android.animation.Animator; import android.animation.AnimatorSet; @@ -31,27 +35,32 @@ import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; +import com.android.launcher3.Hotseat; import com.android.launcher3.LauncherState; -import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.taskbar.bubbles.BubbleBarController; +import com.android.launcher3.taskbar.bubbles.BubbleControllers; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.OnboardingPrefs; +import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.GestureState; import com.android.quickstep.HomeVisibilityState; import com.android.quickstep.LauncherActivityInterface; +import com.android.quickstep.OverviewComponentObserver; import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.SystemUiProxy; -import com.android.quickstep.util.GroupTask; -import com.android.quickstep.util.TISBindHelper; +import com.android.quickstep.fallback.window.RecentsWindowManager; +import com.android.quickstep.util.SplitTask; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.Arrays; @@ -67,32 +76,41 @@ public class LauncherTaskbarUIController extends TaskbarUIController { public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1; public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2; public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3; + public static final int LAUNCHER_PAUSE_PROGRESS_INDEX = 4; - public static final int DISPLAY_PROGRESS_COUNT = 4; + public static final int DISPLAY_PROGRESS_COUNT = 5; private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat( this::onInAppDisplayProgressChanged); - private final MultiPropertyFactory mTaskbarInAppDisplayProgressMultiProp = new MultiPropertyFactory<>( - mTaskbarInAppDisplayProgress, - AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max); + private final MultiPropertyFactory mTaskbarInAppDisplayProgressMultiProp = + new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress, + AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max); + private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat( + this::onLauncherPauseProgressUpdate); private final QuickstepLauncher mLauncher; private final HomeVisibilityState mHomeState; - private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = dp -> { - onStashedInAppChanged(dp); - if (mControllers != null && mControllers.taskbarViewController != null) { - mControllers.taskbarViewController.onRotationChanged(dp); - } - }; - private final HomeVisibilityState.VisibilityChangeListener mVisibilityChangeListener = this::onLauncherVisibilityChanged; + private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = + dp -> { + onStashedInAppChanged(dp); + postAdjustHotseatForBubbleBar(); + if (mControllers != null && mControllers.taskbarViewController != null) { + mControllers.taskbarViewController.onRotationChanged(dp); + } + }; + private final HomeVisibilityState.VisibilityChangeListener mVisibilityChangeListener = + this::onLauncherVisibilityChanged; // Initialized in init. - private final TaskbarLauncherStateController mTaskbarLauncherStateController = new TaskbarLauncherStateController(); + private final TaskbarLauncherStateController + mTaskbarLauncherStateController = new TaskbarLauncherStateController(); + // When overview-in-a-window is enabled, that window is the container, else it is mLauncher. + private RecentsViewContainer mRecentsViewContainer; public LauncherTaskbarUIController(QuickstepLauncher launcher) { mLauncher = launcher; - mHomeState = SystemUiProxy.INSTANCE.get(mLauncher).getHomeVisibilityState(); + mHomeState = SystemUiProxy.INSTANCE.get(mLauncher).getHomeVisibilityState(); } @Override @@ -101,24 +119,30 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mTaskbarLauncherStateController.init(mControllers, mLauncher, mControllers.getSharedState().sysuiStateFlags); - + final TaskbarActivityContext taskbarContext = mControllers.taskbarActivityContext; + int displayId = taskbarContext.getDisplayId(); + BaseContainerInterface containerInterface = OverviewComponentObserver.INSTANCE.get( + taskbarContext).getContainerInterface(displayId); + if (containerInterface != null + && containerInterface.getCreatedContainer() + instanceof RecentsWindowManager recentsWindowManager) { + mRecentsViewContainer = recentsWindowManager; + mRecentsViewContainer.setTaskbarUIController(this); + } else { + mRecentsViewContainer = mLauncher; + } mLauncher.setTaskbarUIController(this); + mHomeState.addListener(mVisibilityChangeListener); - onLauncherVisibilityChanged( - Flags.useActivityOverlay() - ? mHomeState.isHomeVisible() - : mLauncher.hasBeenResumed(), - true /* fromInit */); + onLauncherVisibilityChanged(mHomeState.isHomeVisible(), true /* fromInit */); onStashedInAppChanged(mLauncher.getDeviceProfile()); mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); // Restore the in-app display progress from before Taskbar was recreated. float[] prevProgresses = mControllers.getSharedState().inAppDisplayProgressMultiPropValues; - // Make a copy of the previous progress to set since updating the multiprop will - // update - // the property which also calls onInAppDisplayProgressChanged() which writes - // the current + // Make a copy of the previous progress to set since updating the multiprop will update + // the property which also calls onInAppDisplayProgressChanged() which writes the current // values into the shared state prevProgresses = Arrays.copyOf(prevProgresses, prevProgresses.length); for (int i = 0; i < prevProgresses.length; i++) { @@ -128,12 +152,15 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @Override protected void onDestroy() { + onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */); + mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); super.onDestroy(); - onLauncherVisibilityChanged(false); mTaskbarLauncherStateController.onDestroy(); mLauncher.setTaskbarUIController(null); - mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); + if (mRecentsViewContainer != mLauncher) { + mRecentsViewContainer.setTaskbarUIController(null); + } mHomeState.removeListener(mVisibilityChangeListener); } @@ -141,9 +168,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController { if (mControllers != null) { // Update our shared state so we can restore it if taskbar gets recreated. for (int i = 0; i < DISPLAY_PROGRESS_COUNT; i++) { - mControllers - .getSharedState().inAppDisplayProgressMultiPropValues[i] = mTaskbarInAppDisplayProgressMultiProp - .get(i).getValue(); + mControllers.getSharedState().inAppDisplayProgressMultiPropValues[i] = + mTaskbarInAppDisplayProgressMultiProp.get(i).getValue(); } // Ensure nav buttons react to our latest state if necessary. mControllers.navbarButtonsViewController.updateNavButtonTranslationY(); @@ -152,8 +178,9 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @Override protected boolean isTaskbarTouchable() { - return !(mTaskbarLauncherStateController.isAnimatingToLauncher() - && mTaskbarLauncherStateController.isTaskbarAlignedWithHotseat()); + // Touching down during animation to Hotseat will end the transition and allow the touch to + // go through to the Hotseat directly. + return !isAnimatingToHotseat(); } public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) { @@ -161,19 +188,25 @@ public class LauncherTaskbarUIController extends TaskbarUIController { shouldDelayLauncherStateAnim); } + @Override + public void stashHotseat(boolean stash) { + mTaskbarLauncherStateController.stashHotseat(stash); + } + + @Override + public void unStashHotseatInstantly() { + mTaskbarLauncherStateController.unStashHotseatInstantly(); + } + /** * Adds the Launcher resume animator to the given animator set. * - * This should be used to run a Launcher resume animation whose progress matches - * a + * This should be used to run a Launcher resume animation whose progress matches a * swipe progress. * - * @param placeholderDuration a placeholder duration to be used to ensure all - * full-length - * sub-animations are properly coordinated. This - * duration should not - * actually be used since this animation tracks a - * swipe progress. + * @param placeholderDuration a placeholder duration to be used to ensure all full-length + * sub-animations are properly coordinated. This duration should not + * actually be used since this animation tracks a swipe progress. */ protected void addLauncherVisibilityChangedAnimation(AnimatorSet animation, int placeholderDuration) { @@ -185,51 +218,69 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } /** - * Should be called from onResume() and onPause(), and animates the Taskbar - * accordingly. + * Should be called from onResume() and onPause(), and animates the Taskbar accordingly. */ @Override public void onLauncherVisibilityChanged(boolean isVisible) { + final TaskbarActivityContext taskbarContext = mControllers.taskbarActivityContext; + if (taskbarContext.showLockedTaskbarOnHome() + && !taskbarContext.showDesktopTaskbarForFreeformDisplay() + && taskbarContext.isPrimaryDisplay()) { + DisplayController.INSTANCE.get(mLauncher).notifyConfigChange(); + } + onLauncherVisibilityChanged(isVisible, false /* fromInit */); } - private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInit) { + private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInitOrDestroy) { + if (mControllers == null) { + return; + } onLauncherVisibilityChanged( isVisible, - fromInit, + fromInitOrDestroy, /* startAnimation= */ true, - DisplayController.isTransientTaskbar(mLauncher) - ? TRANSIENT_TASKBAR_TRANSITION_DURATION - : (!isVisible - ? QuickstepTransitionManager.TASKBAR_TO_APP_DURATION - : QuickstepTransitionManager.getTaskbarToHomeDuration())); + getTaskbarAnimationDuration(isVisible)); + } + + private int getTaskbarAnimationDuration(boolean isVisible) { + // fast animation duration since we will not be playing workspace reveal animation. + boolean shouldOverrideToFastAnimation = !isHotseatIconOnTopWhenAligned(); + if (!Flags.predictiveBackToHomePolish()) { + shouldOverrideToFastAnimation |= mLauncher.getPredictiveBackToHomeInProgress(); + } + + boolean isPinned = mControllers.taskbarActivityContext.isPinnedTaskbar(); + if (isVisible || isPinned) { + return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinned); + } else { + return (mControllers.taskbarActivityContext.isTransientTaskbar()) + ? TRANSIENT_TASKBAR_TRANSITION_DURATION : TASKBAR_TO_APP_DURATION; + } } @Nullable private Animator onLauncherVisibilityChanged( - boolean isVisible, boolean fromInit, boolean startAnimation, int duration) { - // Launcher is resumed during the swipe-to-overview gesture under - // shell-transitions, so - // avoid updating taskbar state in that situation (when it's non-interactive -- - // or + boolean isVisible, boolean fromInitOrDestroy, boolean startAnimation, int duration) { + // Launcher is resumed during the swipe-to-overview gesture under shell-transitions, so + // avoid updating taskbar state in that situation (when it's non-interactive -- or // "background") to avoid premature animations. - if (ENABLE_SHELL_TRANSITIONS && isVisible - && mLauncher.getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE) - && !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) { + LauncherState state = mTaskbarLauncherStateController.getLauncherState(); + boolean nonInteractiveState = state.hasFlag(FLAG_NON_INTERACTIVE) + && !state.isTaskbarAlignedWithHotseat(mLauncher); + if (isVisible && (nonInteractiveState || mSkipLauncherVisibilityChange)) { return null; } - DesktopVisibilityController desktopController = LauncherActivityInterface.INSTANCE - .getDesktopVisibilityController(); - if (!enableDesktopWindowingWallpaperActivity() - && desktopController != null - && desktopController.areDesktopTasksVisible()) { + if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() + && mControllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview( + mControllers.taskbarActivityContext.getDisplayId())) { // TODO: b/333533253 - Remove after flag rollout isVisible = false; } mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, isVisible); - if (fromInit) { + if (fromInitOrDestroy) { duration = 0; } return mTaskbarLauncherStateController.applyState(duration, startAnimation); @@ -242,9 +293,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @Override public void refreshResumedState() { - onLauncherVisibilityChanged(Flags.useActivityOverlay() - ? mHomeState.isHomeVisible() - : mLauncher.hasBeenResumed()); + onLauncherVisibilityChanged(mHomeState.isHomeVisible()); } @Override @@ -254,19 +303,58 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } } + private void postAdjustHotseatForBubbleBar() { + Hotseat hotseat = mLauncher.getHotseat(); + if (hotseat == null || !isBubbleBarVisible()) return; + hotseat.post(() -> { + if (mControllers == null) return; + adjustHotseatForBubbleBar(isBubbleBarVisible()); + }); + } + + private boolean isBubbleBarVisible() { + BubbleControllers bubbleControllers = mControllers.bubbleControllers.orElse(null); + return bubbleControllers != null + && bubbleControllers.bubbleBarViewController.isBubbleBarVisible(); + } + /** - * Create Taskbar animation when going from an app to Launcher as part of - * recents transition. - * - * @param toState If known, the state we will end up in when reaching - * Launcher. - * @param callbacks callbacks to track the recents animation lifecycle. The - * state change is - * automatically reset once the recents animation finishes + * Create Taskbar animation when going from an app to Launcher as part of recents transition. + * {@inheritDoc} */ - public Animator createAnimToLauncher(@NonNull LauncherState toState, - @NonNull RecentsAnimationCallbacks callbacks, long duration) { - return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration); + @Override + public Animator getParallelAnimationToGestureEndTarget( + GestureState.GestureEndTarget gestureEndTarget, long duration, + RecentsAnimationCallbacks callbacks) { + return mTaskbarLauncherStateController.createAnimToLauncher( + LauncherActivityInterface.INSTANCE.stateFromGestureEndTarget(gestureEndTarget), + callbacks, + duration); + } + + /** + * Create Taskbar animation to be played alongside the Launcher app launch animation. + */ + public @Nullable Animator createAnimToApp() { + if (!syncAppLaunchWithTaskbarStash()) { + return null; + } + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(TaskbarStashController.FLAG_IN_APP, true); + return stashController.createApplyStateAnimator(stashController.getStashDuration()); + } + + /** + * Temporarily ignore FLAG_IN_APP for app launches to prevent premature taskbar stashing. + * This is needed because taskbar gets a signal to stash before we actually start the + * app launch animation. + */ + public void setIgnoreInAppFlagForSync(boolean enabled) { + if (syncAppLaunchWithTaskbarStash() + && mControllers != null + && mControllers.taskbarStashController != null) { + mControllers.taskbarStashController.updateStateForFlag(FLAG_IGNORE_IN_APP, enabled); + } } public void updateTaskbarLauncherStateGoingHome() { @@ -274,10 +362,6 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mTaskbarLauncherStateController.applyState(); } - public boolean isDraggingItem() { - return mControllers.taskbarDragController.isDragging(); - } - @Override protected void onStashedInAppChanged() { onStashedInAppChanged(mLauncher.getDeviceProfile()); @@ -289,8 +373,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } /** - * Starts a Taskbar EDU flow, if the user should see one upon launching an - * application. + * Starts a Taskbar EDU flow, if the user should see one upon launching an application. */ public void showEduOnAppLaunch() { if (!shouldShowEduOnAppLaunch()) { @@ -300,7 +383,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } // Persistent features EDU tooltip. - if (!DisplayController.isTransientTaskbar(mLauncher)) { + if (!mControllers.taskbarActivityContext.isTransientTaskbar()) { mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); return; } @@ -315,8 +398,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } /** - * Returns {@code true} if a Taskbar education should be shown on application - * launch. + * Returns {@code true} if a Taskbar education should be shown on application launch. */ public boolean shouldShowEduOnAppLaunch() { if (Utilities.isRunningInTestHarness()) { @@ -324,7 +406,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } // Persistent features EDU tooltip. - if (!DisplayController.isTransientTaskbar(mLauncher)) { + if (!mControllers.taskbarActivityContext.isTransientTaskbar()) { return !OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.hasReachedMax(mLauncher); } @@ -341,8 +423,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } /** - * Animates Taskbar elements during a transition to a Launcher state that should - * use in-app + * Animates Taskbar elements during a transition to a Launcher state that should use in-app * layouts. * * @param progress [0, 1] @@ -355,16 +436,22 @@ public class LauncherTaskbarUIController extends TaskbarUIController { // This method can be called before init() is called. return; } - if (mControllers.uiController.isIconAlignedWithHotseat() - && !mTaskbarLauncherStateController.isAnimatingToLauncher()) { - // Only animate the nav buttons while home and not animating home, otherwise let - // the TaskbarViewController handle it. - mControllers.navbarButtonsViewController - .getTaskbarNavButtonTranslationYForInAppDisplay() - .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY() - * mTaskbarInAppDisplayProgress.value); - mControllers.navbarButtonsViewController - .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress); + if (mControllers.uiController.isIconAlignedWithHotseat()) { + if (!mTaskbarLauncherStateController.isAnimatingToLauncher()) { + // Only animate the nav buttons while home and not animating home, otherwise let + // the TaskbarViewController handle it. + mControllers.navbarButtonsViewController + .getTaskbarNavButtonTranslationYForInAppDisplay() + .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY() + * mTaskbarInAppDisplayProgress.value); + mControllers.navbarButtonsViewController + .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress); + } + if (isBubbleBarEnabled()) { + mControllers.bubbleControllers.ifPresent( + c -> c.bubbleStashController.setInAppDisplayOverrideProgress( + mTaskbarInAppDisplayProgress.value)); + } } } @@ -409,7 +496,18 @@ public class LauncherTaskbarUIController extends TaskbarUIController { public boolean isHotseatIconOnTopWhenAligned() { return mTaskbarLauncherStateController.isInHotseatOnTopStates() && mTaskbarInAppDisplayProgressMultiProp.get(MINUS_ONE_PAGE_PROGRESS_INDEX) - .getValue() == 0; + .getValue() == 0; + } + + @Override + public boolean isAnimatingToHotseat() { + return mTaskbarLauncherStateController.isAnimatingToLauncher() + && isIconAlignedWithHotseat(); + } + + @Override + public void endAnimationToHotseat() { + mTaskbarLauncherStateController.resetIconAlignment(); } @Override @@ -418,21 +516,26 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } @Override - protected boolean canToggleHomeAllApps() { - return mLauncher.isResumed() + protected void toggleAllApps(boolean focusSearch) { + boolean canToggleHomeAllApps = mLauncher.isResumed() && !mTaskbarLauncherStateController.isInOverviewUi() && !mLauncher.areDesktopTasksVisible(); + if (canToggleHomeAllApps) { + mLauncher.toggleAllApps(focusSearch); + return; + } + super.toggleAllApps(focusSearch); } @Override public RecentsView getRecentsView() { - return mLauncher.getOverviewPanel(); + return mRecentsViewContainer.getOverviewPanel(); } @Override public void launchSplitTasks( - @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { - mLauncher.launchSplitTasks(groupTask, remoteTransition); + @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) { + mLauncher.launchSplitTasks(splitTask, remoteTransition); } @Override @@ -440,12 +543,6 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mTaskbarLauncherStateController.resetIconAlignment(); } - @Nullable - @Override - protected TISBindHelper getTISBindHelper() { - return mLauncher.getTISBindHelper(); - } - @Override public void dumpLogs(String prefix, PrintWriter pw) { super.dumpLogs(prefix, pw); @@ -459,7 +556,9 @@ public class LauncherTaskbarUIController extends TaskbarUIController { "MINUS_ONE_PAGE_PROGRESS_INDEX", "ALL_APPS_PAGE_PROGRESS_INDEX", "WIDGETS_PAGE_PROGRESS_INDEX", - "SYSUI_SURFACE_PROGRESS_INDEX"); + "SYSUI_SURFACE_PROGRESS_INDEX", + "LAUNCHER_PAUSE_PROGRESS_INDEX"); + pw.println(String.format("%s\tmRecentsWindowContainer=%s", prefix, mRecentsViewContainer)); mTaskbarLauncherStateController.dumpLogs(prefix + "\t", pw); } @@ -468,4 +567,60 @@ public class LauncherTaskbarUIController extends TaskbarUIController { protected String getTaskbarUIControllerName() { return "LauncherTaskbarUIController"; } + + @Override + public void onBubbleBarLocationAnimated(BubbleBarLocation location) { + mTaskbarLauncherStateController.onBubbleBarLocationChanged(location, /* animate = */ true); + mLauncher.setBubbleBarLocation(location); + } + + @Override + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + mTaskbarLauncherStateController.onBubbleBarLocationChanged(location, /* animate = */ false); + mLauncher.setBubbleBarLocation(location); + } + + @Override + public void onSwipeToUnstashTaskbar() { + // Once taskbar is unstashed, the user cannot return back to the overlay. We can + // clear it here to set the expected state once the user goes home. + if (mLauncher.getWorkspace().isOverlayShown()) { + mLauncher.getWorkspace().onOverlayScrollChanged(0); + } + } + + /** + * Called when Launcher Activity resumed while staying at home. + *

+ * Shift nav buttons up to at-home position. + */ + public void onLauncherResume() { + mLauncherPauseProgress.animateToValue(0.0f).start(); + } + + /** + * Called when Launcher Activity paused while staying at home. + *

+ * To avoid UI clash between taskbar & bottom sheet, shift nav buttons down to in-app position. + */ + public void onLauncherPause() { + mLauncherPauseProgress.animateToValue(1.0f).start(); + } + + /** + * On launcher stop, avoid animating taskbar & overriding pre-existing animations. + */ + public void onLauncherStop() { + mLauncherPauseProgress.cancelAnimation(); + mLauncherPauseProgress.updateValue(0.0f); + } + + private void onLauncherPauseProgressUpdate() { + // If we are not aligned with hotseat, setting this will clobber the 3 button nav position. + // So in that case, treat the progress as 0 instead. + float pauseProgress = isIconAlignedWithHotseat() ? mLauncherPauseProgress.value : 0; + onTaskbarInAppDisplayProgressUpdate(pauseProgress, LAUNCHER_PAUSE_PROGRESS_INDEX); + } + + } diff --git a/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt new file mode 100644 index 0000000000..2d6f1ee5be --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt @@ -0,0 +1,275 @@ +/* + * 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.launcher3.taskbar + +import android.content.Context +import android.graphics.Bitmap +import android.view.MotionEvent +import android.view.View +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext +import com.android.launcher3.util.TouchController +import com.android.launcher3.views.ActivityContext +import com.android.quickstep.RecentsModel +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.util.DesktopTask +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason +import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer + +/** + * A single menu item shortcut to execute displaying open instances of an app. Default interaction + * for [onClick] is to open the menu in a floating window. Touching one of the displayed tasks + * launches it. + */ +class ManageWindowsTaskbarShortcut( + private val target: T, + private val itemInfo: ItemInfo?, + private val originalView: View, + private val controllers: TaskbarControllers, +) : + SystemShortcut( + R.drawable.desktop_mode_ic_taskbar_menu_manage_windows, + R.string.manage_windows_option_taskbar, + target, + itemInfo, + originalView, + ) where T : Context?, T : ActivityContext? { + private lateinit var taskbarShortcutAllWindowsView: TaskbarShortcutManageWindowsView + private val recentsModel = RecentsModel.INSTANCE[controllers.taskbarActivityContext] + + override fun onClick(v: View?) { + val targetPackage = itemInfo?.getTargetPackage() + val targetUserId = itemInfo?.user?.identifier + val isTargetPackageTask: (Task) -> Boolean = { task -> + task.key?.packageName == targetPackage && task.key.userId == targetUserId + } + + recentsModel.getTasks { tasks -> + val desktopTask = tasks.filterIsInstance().firstOrNull() + val packageDesktopTasks = + (desktopTask?.tasks ?: emptyList()).filter(isTargetPackageTask) + val nonDesktopPackageTasks = + tasks.flatMap { it.tasks }.filter { isTargetPackageTask(it) } + + // Add tasks from the fetched tasks, deduplicating by task ID + val packageTasks = + (packageDesktopTasks + nonDesktopPackageTasks).distinctBy { it.key.id } + + // Since fetching thumbnails is asynchronous, use `awaitedTaskIds` to gate until the + // tasks are ready to display + val awaitedTaskIds = packageTasks.map { it.key.id }.toMutableSet() + + createAndShowTaskShortcutView(packageTasks, awaitedTaskIds) + } + } + + /** + * Processes a list of tasks to generate thumbnails and create a taskbar shortcut view. + * + * Iterates through the tasks, retrieves thumbnails, and adds them to a list. When all + * thumbnails are processed, it creates a [TaskbarShortcutManageWindowsView] with the collected + * thumbnails and positions it appropriately. + */ + private fun createAndShowTaskShortcutView(tasks: List, pendingTaskIds: MutableSet) { + val taskList = arrayListOf>() + + tasks.forEach { task -> + recentsModel.thumbnailCache.getThumbnailInBackground(task) { + thumbnailData: ThumbnailData -> + pendingTaskIds.remove(task.key.id) + // Add the current pair of task id and ThumbnailData to the list of all tasks + if (thumbnailData.thumbnail != null) { + taskList.add(task.key.id to thumbnailData.thumbnail) + } + // If the set is empty, all thumbnails have been fetched + if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) { + createAndPositionTaskbarShortcut(taskList) + } + } + } + } + + /** + * Creates and positions the [TaskbarShortcutManageWindowsView] with the provided thumbnails. + */ + private fun createAndPositionTaskbarShortcut(taskList: ArrayList>) { + val onIconClickListener = + ({ taskId: Int? -> + taskbarShortcutAllWindowsView.animateClose() + if (taskId != null) { + SystemUiProxy.INSTANCE.get(target) + .showDesktopApp( + taskId, + /* transition= */ null, + DesktopTaskToFrontReason.TASKBAR_MANAGE_WINDOW, + ) + } + }) + + val onOutsideClickListener = { taskbarShortcutAllWindowsView.animateClose() } + + taskbarShortcutAllWindowsView = + TaskbarShortcutManageWindowsView( + originalView, + controllers.taskbarOverlayController.requestWindow(), + taskList, + onIconClickListener, + onOutsideClickListener, + controllers, + ) + + // If the view is removed from elsewhere, reset the state to allow the taskbar to auto-stash + taskbarShortcutAllWindowsView.menuView.rootView.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + return + } + + override fun onViewDetachedFromWindow(v: View) { + controllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, + false, + ) + controllers.taskbarPopupController.cleanUpMultiInstanceMenuReference() + } + } + ) + } + + /** Closes the multi-instance menu if it has been initialized. */ + fun closeMultiInstanceMenu() { + if (::taskbarShortcutAllWindowsView.isInitialized) { + taskbarShortcutAllWindowsView.animateClose() + } + } + + /** + * A view container for displaying the window of open instances of an app + * + * Handles showing the window snapshots, adding the carousel to the overlay, and closing it. + * Also acts as a touch controller to intercept touch events outside the carousel to close it. + */ + class TaskbarShortcutManageWindowsView( + private val originalView: View, + private val taskbarOverlayContext: TaskbarOverlayContext, + snapshotList: ArrayList>, + onIconClickListener: (Int) -> Unit, + onOutsideClickListener: () -> Unit, + private val controllers: TaskbarControllers, + ) : + ManageWindowsViewContainer( + originalView.context, + originalView.context.getColor(R.color.materialColorSurfaceBright), + ), + TouchController { + private val taskbarActivityContext = controllers.taskbarActivityContext + + init { + createAndShowMenuView(snapshotList, onIconClickListener, onOutsideClickListener) + taskbarOverlayContext.dragLayer.addTouchController(this) + animateOpen() + } + + /** Adds the carousel menu to the taskbar overlay drag layer */ + override fun addToContainer(menuView: ManageWindowsView) { + positionCarouselMenu() + + controllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, + true, + ) + AbstractFloatingView.closeAllOpenViewsExcept( + taskbarActivityContext, + AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY, + ) + menuView.rootView.minimumHeight = menuView.menuHeight + menuView.rootView.minimumWidth = menuView.menuWidth + + taskbarOverlayContext.dragLayer?.addView(menuView.rootView) + menuView.rootView.requestFocus() + } + + /** + * Positions the carousel menu relative to the taskbar and the calling app's icon. + * + * Calculates the Y position to place the carousel above the taskbar, and the X position to + * align with the calling app while ensuring it doesn't go beyond the screen edge. + */ + private fun positionCarouselMenu() { + val deviceProfile = taskbarActivityContext.deviceProfile + val margin = + context.resources.getDimension( + R.dimen.taskbar_multi_instance_menu_min_padding_from_screen_edge + ) + + // Calculate the Y position to place the carousel above the taskbar + menuView.rootView.y = + deviceProfile.deviceProperties.availableHeightPx - + menuView.menuHeight - + controllers.taskbarStashController.touchableHeight - + margin + + // Calculate the X position to align with the calling app, + // but avoid clashing with the screen edge + menuView.rootView.translationX = + if (Utilities.isRtl(context.resources)) { + -(deviceProfile.deviceProperties.availableWidthPx - menuView.menuWidth) / 2f + } else { + val maxX = + deviceProfile.deviceProperties.availableWidthPx - + menuView.menuWidth - + margin + minOf(originalView.x, maxX) + } + } + + /** Closes the carousel menu and removes it from the taskbar overlay drag layer */ + override fun removeFromContainer() { + controllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, + false, + ) + taskbarOverlayContext.dragLayer?.removeView(menuView.rootView) + taskbarOverlayContext.dragLayer.removeTouchController(this) + controllers.taskbarPopupController.cleanUpMultiInstanceMenuReference() + } + + /** TouchController implementations for closing the carousel when touched outside */ + override fun onControllerTouchEvent(ev: MotionEvent?): Boolean { + return false + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { + ev?.let { + if ( + it.action == MotionEvent.ACTION_DOWN && + !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, it) + ) { + animateClose() + } + } + return false + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index 293c2b2a65..756ddb4169 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static com.android.launcher3.LauncherAnimUtils.ROTATION_DRAWABLE_PERCENT; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y; @@ -38,21 +39,27 @@ import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VAL import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; +import static com.android.window.flags.Flags.predictiveBackThreeButtonNav; +import android.animation.Animator; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.DrawableRes; import android.annotation.IdRes; import android.annotation.LayoutRes; +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.ActivityInfo.Config; import android.content.res.ColorStateList; @@ -60,24 +67,27 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Region; import android.graphics.Region.Op; -import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.graphics.drawable.RotateDrawable; -import android.inputmethodservice.InputMethodService; +import android.os.Bundle; import android.os.Handler; +import android.os.SystemProperties; import android.util.Property; import android.view.Gravity; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnAttachStateChangeListener; -import android.view.View.OnClickListener; -import android.view.View.OnHoverListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.Flags; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -92,11 +102,11 @@ import com.android.launcher3.Utilities; import com.android.launcher3.anim.AlphaUpdateListener; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton; +import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory; import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter; import com.android.launcher3.taskbar.navbutton.NearestTouchFrame; import com.android.launcher3.util.DimensionUtils; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.TouchController; @@ -105,25 +115,37 @@ import com.android.launcher3.views.BaseDragLayer; import com.android.systemui.shared.navigationbar.KeyButtonRipple; import com.android.systemui.shared.rotation.FloatingRotationButton; import com.android.systemui.shared.rotation.RotationButton; -import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.ArrayList; import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.IntPredicate; /** * Controller for managing nav bar buttons in taskbar */ -public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController { +public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController, + BubbleBarController.BubbleBarLocationListener { private final Rect mTempRect = new Rect(); - private static final int FLAG_SWITCHER_SHOWING = 1 << 0; + /** Whether the IME Switcher button is visible. */ + private static final int FLAG_IME_SWITCHER_BUTTON_VISIBLE = 1 << 0; + /** Whether the IME is visible. */ private static final int FLAG_IME_VISIBLE = 1 << 1; - private static final int FLAG_ROTATION_BUTTON_VISIBLE = 1 << 2; + /** + * The back button is visually adjusted to indicate that it will dismiss the IME when pressed. + * This only takes effect while the IME is visible. By default, it is set while the IME is + * visible, but may be overridden by the + * {@link android.inputmethodservice.InputMethodService.BackDispositionMode backDispositionMode} + * set by the IME. + */ + private static final int FLAG_BACK_DISMISS_IME = 1 << 2; private static final int FLAG_A11Y_VISIBLE = 1 << 3; private static final int FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE = 1 << 4; private static final int FLAG_KEYGUARD_VISIBLE = 1 << 5; @@ -139,13 +161,15 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private static final int FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING = 1 << 15; /** - * Flags where a UI could be over Taskbar surfaces, so the color override should - * be disabled. + * Flags where a UI could be over Taskbar surfaces, so the color override should be disabled. */ - private static final int FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED = FLAG_NOTIFICATION_SHADE_EXPANDED - | FLAG_VOICE_INTERACTION_WINDOW_SHOWING; + private static final int FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED = + FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_VOICE_INTERACTION_WINDOW_SHOWING; private static final String NAV_BUTTONS_SEPARATE_WINDOW_TITLE = "Taskbar Nav Buttons"; + private static final String SUW_THEME_SYSTEM_PROPERTY = "setupwizard.theme"; + private static final String GLIF_EXPRESSIVE_THEME = "glif_expressive"; + private static final String GLIF_EXPRESSIVE_LIGHT_THEME = "glif_expressive_light"; private static final double SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95; private static final double SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05; @@ -155,6 +179,9 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT public static final int ALPHA_INDEX_SUW = 2; private static final int NUM_ALPHA_CHANNELS = 3; + private static final long AUTODIM_TIMEOUT_MS = 2250; + private static final long PREDICTIVE_BACK_TIMEOUT_MS = 200; + private final ArrayList mPropertyHolders = new ArrayList<>(); private final ArrayList mAllButtons = new ArrayList<>(); private int mState; @@ -163,17 +190,19 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private final @Nullable Context mNavigationBarPanelContext; private final WindowManagerProxy mWindowManagerProxy; private final NearestTouchFrame mNavButtonsView; + private final Handler mHandler; private final LinearLayout mNavButtonContainer; // Used for IME+A11Y buttons private final ViewGroup mEndContextualContainer; private final ViewGroup mStartContextualContainer; - private final int mLightIconColorOnHome; - private final int mDarkIconColorOnHome; - /** - * Color to use for navigation bar buttons, if they are on on a Taskbar surface - * background. - */ + private final int mLightIconColorOnWorkspace; + private final int mDarkIconColorOnWorkspace; + /** Color to use for navbar buttons, if they are on on a Taskbar surface background. */ private final int mOnBackgroundIconColor; + private final boolean mIsExpressiveThemeEnabled; + + private @Nullable Animator mNavBarLocationAnimator; + private @Nullable BubbleBarLocation mBubbleBarTargetLocation; private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat( this::updateNavButtonTranslationY); @@ -182,13 +211,15 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat( this::updateNavButtonTranslationY); private float mLastSetNavButtonTranslationY; - // Used for System UI state updates that should translate the nav button for - // in-app display. + // Used for System UI state updates that should translate the nav button for in-app display. private final AnimatedFloat mNavButtonInAppDisplayProgressForSysui = new AnimatedFloat( this::updateNavButtonInAppDisplayProgressForSysui); - /** Expected nav button dark intensity communicated via the framework. */ + /** + * Expected nav button dark intensity piped down from {@code LightBarController} in framework + * via {@code TaskbarDelegate}. + */ private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat( - this::updateNavButtonColor); + this::onDarkIntensityChanged); /** {@code 1} if the Taskbar background color is fully opaque. */ private final AnimatedFloat mOnTaskbarBackgroundNavButtonColorOverride = new AnimatedFloat( this::updateNavButtonColor); @@ -218,26 +249,41 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT // Variables for moving nav buttons to a separate window above IME private boolean mAreNavButtonsInSeparateWindow = false; private BaseDragLayer mSeparateWindowParent; // Initialized in init. - private final ViewTreeObserver.OnComputeInternalInsetsListener mSeparateWindowInsetsComputer = this::onComputeInsetsForSeparateWindow; + private final ViewTreeObserver.OnComputeInternalInsetsListener mSeparateWindowInsetsComputer = + this::onComputeInsetsForSeparateWindow; private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender(); private ImageView mRecentsButton; private Space mSpace; + private TaskbarTransitions mTaskbarTransitions; + private @BarTransitions.TransitionMode int mTransitionMode; + + private final Runnable mAutoDim = () -> mTaskbarTransitions.setAutoDim(true); + public NavbarButtonsViewController(TaskbarActivityContext context, - @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) { + @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView, + Handler handler) { mContext = context; mNavigationBarPanelContext = navigationBarPanelContext; mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext); mNavButtonsView = navButtonsView; + mHandler = handler; mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons); mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons); mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons); - mLightIconColorOnHome = context.getColor(R.color.taskbar_nav_icon_light_color_on_home); - mDarkIconColorOnHome = context.getColor(R.color.taskbar_nav_icon_dark_color_on_home); + mLightIconColorOnWorkspace = context.getColor(R.color.taskbar_nav_icon_light_color_on_home); + mDarkIconColorOnWorkspace = context.getColor(R.color.taskbar_nav_icon_dark_color_on_home); mOnBackgroundIconColor = Utilities.isDarkTheme(context) ? context.getColor(R.color.taskbar_nav_icon_light_color) : context.getColor(R.color.taskbar_nav_icon_dark_color); + + if (mContext.isPhoneMode()) { + mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView); + } + String SUWTheme = SystemProperties.get(SUW_THEME_SYSTEM_PROPERTY, ""); + mIsExpressiveThemeEnabled = SUWTheme.equals(GLIF_EXPRESSIVE_THEME) + || SUWTheme.equals(GLIF_EXPRESSIVE_LIGHT_THEME); } /** @@ -249,26 +295,37 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } protected void setupController() { - boolean isThreeButtonNav = mContext.isThreeButtonNav(); + final boolean isThreeButtonNav = mContext.isThreeButtonNav(); + final boolean isPhoneMode = mContext.isPhoneMode(); DeviceProfile deviceProfile = mContext.getDeviceProfile(); Resources resources = mContext.getResources(); - Point p = !mContext.isUserSetupComplete() - ? new Point(0, mControllers.taskbarActivityContext.getSetupWindowSize()) - : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources, - mContext.isPhoneMode()); - mNavButtonsView.getLayoutParams().height = p.y; + + int setupSize = mControllers.taskbarActivityContext.getSetupWindowSize(); + Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources, isPhoneMode, + mContext.isGestureNav()); + ViewGroup.LayoutParams navButtonsViewLayoutParams = mNavButtonsView.getLayoutParams(); + navButtonsViewLayoutParams.width = p.x; + if (!mContext.isUserSetupComplete()) { + // Setup mode in phone mode uses gesture nav. + navButtonsViewLayoutParams.height = setupSize; + } else { + navButtonsViewLayoutParams.height = p.y; + } + mNavButtonsView.setLayoutParams(navButtonsViewLayoutParams); mIsImeRenderingNavButtons = mContext.imeDrawsImeNavBar(); -// mDisplayController = DisplayController.INSTANCE.get(mContext); - if (!mIsImeRenderingNavButtons) { // IME switcher - mImeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH, + final int switcherResId = Flags.imeSwitcherRevamp() + ? com.android.internal.R.drawable.ic_ime_switcher_new + : R.drawable.ic_ime_switcher; + mImeSwitcherButton = addButton(switcherResId, BUTTON_IME_SWITCH, isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer, mControllers.navButtonController, R.id.ime_switcher); + // A11y and IME Switcher buttons overlap on phone mode, show only a11y if both visible. mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton, - flags -> ((flags & FLAG_SWITCHER_SHOWING) != 0) - && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0))); + flags -> (flags & FLAG_IME_SWITCHER_BUTTON_VISIBLE) != 0 + && !(isPhoneMode && (flags & FLAG_A11Y_VISIBLE) != 0))); } mPropertyHolders.add(new StatePropertyHolder( @@ -282,62 +339,61 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT .get(ALPHA_INDEX_SMALL_SCREEN), flags -> (flags & FLAG_SMALL_SCREEN) == 0)); - mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController - .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0)); - - // Force nav buttons (specifically back button) to be visible during setup - // wizard. - boolean isInSetup = !mContext.isUserSetupComplete(); - boolean isInKidsMode = mContext.isNavBarKidsModeActive(); - boolean alwaysShowButtons = isThreeButtonNav || isInSetup; - - // Make sure to remove nav bar buttons translation when any of the following - // occur: - // - Notification shade is expanded - // - IME is showing (add separate translation for IME) - // - VoiceInteractionWindow (assistant) is showing - // - Keyboard shortcuts helper is showing - int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE - | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING; - mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui, - flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE, - 1, 0)); - // Center nav buttons in new height for IME. - float transForIme = (mContext.getDeviceProfile().taskbarHeight - - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f; - // For gesture nav, nav buttons only show for IME anyway so keep them translated - // down. - float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme; - mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme, - flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE, - transForIme, defaultButtonTransY)); + if (!isPhoneMode) { + mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController + .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0)); + } // Start at 1 because relevant flags are unset at init. mOnBackgroundNavButtonColorOverrideMultiplier.value = 1; - mPropertyHolders.add(new StatePropertyHolder( - mOnBackgroundNavButtonColorOverrideMultiplier, - flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0)); - mPropertyHolders.add(new StatePropertyHolder( - mSlideInViewVisibleNavButtonColorOverride, - flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0)); + // Potentially force the back button to be visible during setup wizard. The back button + // won't show up if the expressive theme is enabled and simple view is disabled + boolean shouldShowInSetup = !mContext.isUserSetupComplete() + && (!mIsExpressiveThemeEnabled || mContext.isSimpleViewEnabled()); + boolean isInKidsMode = mContext.isNavBarKidsModeActive(); + boolean alwaysShowButtons = isThreeButtonNav || shouldShowInSetup; + + // Make sure to remove nav bar buttons translation when any of the following occur: + // - Notification shade is expanded + // - IME is visible (add separate translation for IME) + // - VoiceInteractionWindow (assistant) is showing + // - Keyboard shortcuts helper is showing + if (!isPhoneMode) { + int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE + | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING; + mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui, + flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE, + 1, 0)); + // Center nav buttons in new height for IME. + float transForIme = (mContext.getDeviceProfile().getTaskbarProfile().getHeight() + - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f; + // For gesture nav, nav buttons only show for IME anyway so keep them translated down. + float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme; + mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme, + flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE, + transForIme, defaultButtonTransY)); + + mPropertyHolders.add(new StatePropertyHolder( + mOnBackgroundNavButtonColorOverrideMultiplier, + flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0)); + + mPropertyHolders.add(new StatePropertyHolder( + mSlideInViewVisibleNavButtonColorOverride, + flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0)); + } if (alwaysShowButtons) { initButtons(mNavButtonContainer, mEndContextualContainer, mControllers.navButtonController); updateButtonLayoutSpacing(); - updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneButtonNavMode()); + updateStateForFlag(FLAG_SMALL_SCREEN, isPhoneMode); - mPropertyHolders.add(new StatePropertyHolder( - mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(), - flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0)); - } else if (!mIsImeRenderingNavButtons) { - View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, - mStartContextualContainer, mControllers.navButtonController, R.id.back); - imeDownButton.setRotation(Utilities.isRtl(resources) ? 90 : -90); - // Only show when IME is visible. - mPropertyHolders.add(new StatePropertyHolder(imeDownButton, - flags -> (flags & FLAG_IME_VISIBLE) != 0)); + if (!isPhoneMode) { + mPropertyHolders.add(new StatePropertyHolder( + mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(), + flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0)); + } } mFloatingRotationButton = new FloatingRotationButton( ENABLE_TASKBAR_NAVBAR_UNIFICATION ? mNavigationBarPanelContext : mContext, @@ -353,14 +409,18 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT R.bool.floating_rotation_button_position_left); mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton, mRotationButtonListener); + if (isPhoneMode) { + mTaskbarTransitions.init(); + } applyState(); mPropertyHolders.forEach(StatePropertyHolder::endAnimation); // Initialize things needed to move nav buttons to separate window. - mSeparateWindowParent = new BaseDragLayer(mContext, null, 0) { + mSeparateWindowParent = new BaseDragLayer<>(mContext, null, 0) { @Override public void recreateControllers() { + super.recreateControllers(); mControllers = new TouchController[0]; } @@ -372,6 +432,12 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } }; mSeparateWindowParent.recreateControllers(); + if (BubbleBarController.isBubbleBarEnabled()) { + mNavButtonsView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + onLayoutsUpdated() + ); + } } private void initButtons(ViewGroup navContainer, ViewGroup endContainer, @@ -400,16 +466,19 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT flags -> (flags & FLAG_IME_VISIBLE) == 0)); } mPropertyHolders.add(new StatePropertyHolder(mBackButton, - flags -> (flags & FLAG_IME_VISIBLE) != 0, + flags -> (flags & FLAG_BACK_DISMISS_IME) != 0, ROTATION_DRAWABLE_PERCENT, 1f, 0f)); - // Translate back button to be at end/start of other buttons for keyguard + // Translate back button to be at end/start of other buttons for keyguard (only after SUW + // since it is laid to align with SUW actions while in that state) int navButtonSize = mContext.getResources().getDimensionPixelSize( R.dimen.taskbar_nav_buttons_size); boolean isRtl = Utilities.isRtl(mContext.getResources()); if (!mContext.isPhoneMode()) { mPropertyHolders.add(new StatePropertyHolder( - mBackButton, flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0 - || (flags & FLAG_KEYGUARD_VISIBLE) != 0, + mBackButton, flags -> mContext.isUserSetupComplete() + && ((flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0 + || (flags & FLAG_KEYGUARD_VISIBLE) != 0) + && (!shouldShowHomeButtonInLockscreen(flags)), VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0)); } @@ -419,10 +488,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mHomeButtonAlpha = new MultiValueAlpha(mHomeButton, NUM_ALPHA_CHANNELS); mHomeButtonAlpha.setUpdateVisibility(true); mPropertyHolders.add( - new StatePropertyHolder(mHomeButtonAlpha.get( - ALPHA_INDEX_KEYGUARD_OR_DISABLE), - flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 - && (flags & FLAG_DISABLE_HOME) == 0 && !mContext.isGestureNav())); + new StatePropertyHolder(mHomeButtonAlpha.get(ALPHA_INDEX_KEYGUARD_OR_DISABLE), + this::shouldShowHomeButtonInLockscreen)); // Recents button mRecentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS, @@ -438,6 +505,13 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT navButtonController.onButtonClick(BUTTON_RECENTS, v); mHitboxExtender.onRecentsButtonClicked(); }); + mRecentsButton.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + int[] location = v.getLocationOnScreen(); + Rect bounds = new Rect(location[0], location[1], location[0] + v.getWidth(), + location[1] + v.getHeight()); + navButtonController.onRecentsButtonLayoutChanged(bounds); + }); mPropertyHolders.add(new StatePropertyHolder(mRecentsButton, flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_RECENTS) == 0 && !mContext.isNavBarKidsModeActive() && !mContext.isGestureNav())); @@ -447,32 +521,68 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT endContainer, navButtonController, R.id.accessibility_button, R.layout.taskbar_contextual_button); mPropertyHolders.add(new StatePropertyHolder(mA11yButton, - flags -> (flags & FLAG_A11Y_VISIBLE) != 0 - && (flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0)); + flags -> (flags & FLAG_A11Y_VISIBLE) != 0)); mSpace = new Space(mNavButtonsView.getContext()); mSpace.setOnClickListener(view -> navButtonController.onButtonClick(BUTTON_SPACE, view)); - mSpace.setOnLongClickListener(view -> navButtonController.onButtonLongClick(BUTTON_SPACE, view)); + mSpace.setOnLongClickListener(view -> + navButtonController.onButtonLongClick(BUTTON_SPACE, view)); + } + + /** + * Method to determine whether the Navigation Bar is viewable in Setup Wizard + * + * @return {@code true} if the device is in Setup Wizard, the expressive theme is enabled, + * and Simple View is NOT enabled + */ + boolean isNavbarHiddenInSUW() { + if (mContext == null) { + return false; + } + return !mContext.isUserSetupComplete() && mIsExpressiveThemeEnabled + && !mContext.isSimpleViewEnabled(); + } + + /** + * Method to determine whether to show the home button in lockscreen + * + * When the keyguard is visible hide home button. Anytime we are + * occluded we want to show the home button for apps over keyguard. + * however we don't want to show when not occluded/visible. + * (visible false || occluded true) && disable false && not gnav + */ + private boolean shouldShowHomeButtonInLockscreen(int flags) { + return ((flags & FLAG_KEYGUARD_VISIBLE) == 0 + || (flags & FLAG_KEYGUARD_OCCLUDED) != 0) + && (flags & FLAG_DISABLE_HOME) == 0 + && !mContext.isGestureNav(); } private void parseSystemUiFlags(@SystemUiStateFlags long sysUiStateFlags) { mSysuiStateFlags = sysUiStateFlags; - boolean isImeVisible = (sysUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0; - boolean isImeSwitcherShowing = (sysUiStateFlags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0; + boolean isImeSwitcherButtonVisible = + (sysUiStateFlags & SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE) != 0; + boolean isImeVisible = (sysUiStateFlags & SYSUI_STATE_IME_VISIBLE) != 0; + boolean isBackDismissIme = (sysUiStateFlags & SYSUI_STATE_BACK_DISMISS_IME) != 0; boolean a11yVisible = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0; boolean isHomeDisabled = (sysUiStateFlags & SYSUI_STATE_HOME_DISABLED) != 0; - boolean isRecentsDisabled = (sysUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0; + // TODO: b/409075366 - ensure this signal is correctly set for external displays. + boolean isRecentsDisabled = mContext.isPrimaryDisplay() + && (sysUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0; boolean isBackDisabled = (sysUiStateFlags & SYSUI_STATE_BACK_DISABLED) != 0; long shadeExpandedFlags = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; boolean isNotificationShadeExpanded = (sysUiStateFlags & shadeExpandedFlags) != 0; boolean isScreenPinningActive = (sysUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0; - boolean isVoiceInteractionWindowShowing = (sysUiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0; - boolean isKeyboardShortcutHelperShowing = (sysUiStateFlags & SYSUI_STATE_SHORTCUT_HELPER_SHOWING) != 0; - - // TODO(b/202218289) we're getting IME as not visible on lockscreen from system + boolean isVoiceInteractionWindowShowing = + (sysUiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0; + boolean isKeyboardShortcutHelperShowing = + (sysUiStateFlags & SYSUI_STATE_SHORTCUT_HELPER_SHOWING) != 0; + boolean splitAnimationRunning = + (sysUiStateFlags & SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION) != 0; + updateStateForFlag(FLAG_IME_SWITCHER_BUTTON_VISIBLE, isImeSwitcherButtonVisible); updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible); - updateStateForFlag(FLAG_SWITCHER_SHOWING, isImeSwitcherShowing); + updateStateForFlag(FLAG_BACK_DISMISS_IME, isBackDismissIme); updateStateForFlag(FLAG_A11Y_VISIBLE, a11yVisible); updateStateForFlag(FLAG_DISABLE_HOME, isHomeDisabled); updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled); @@ -484,10 +594,17 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT if (mA11yButton != null) { // Only used in 3 button - boolean a11yLongClickable = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0; + boolean a11yLongClickable = + (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0; mA11yButton.setLongClickable(a11yLongClickable); updateButtonLayoutSpacing(); } + + if (mNavButtonContainer.getChildCount() > 0) { + for (int i = 0; i < mNavButtonContainer.getChildCount(); i++) { + mNavButtonContainer.getChildAt(i).setEnabled(!splitAnimationRunning); + } + } } public void updateStateForSysuiFlags(@SystemUiStateFlags long systemUiStateFlags, @@ -519,8 +636,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT /** * Slightly misnamed, but should be called when keyguard OR AOD is showing. - * We consider keyguardVisible when it's showing bouncer OR is occlucded by - * another app + * We consider keyguardVisible when it's showing bouncer OR is occlucded by another app */ public void setKeyguardVisible(boolean isKeyguardVisible, boolean isKeyguardOccluded) { updateStateForFlag(FLAG_KEYGUARD_VISIBLE, isKeyguardVisible || isKeyguardOccluded); @@ -556,7 +672,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT * Returns true if the recents (overview) button is disabled */ public boolean isRecentsDisabled() { - return (mState & FLAG_DISABLE_RECENTS) != 0; + // TODO: b/409075366 - ensure this signal is correctly set for external displays. + return (mState & FLAG_DISABLE_RECENTS) != 0 && mContext.isPrimaryDisplay(); } /** @@ -602,12 +719,68 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT /** * Sets the AccessibilityDelegate for the back button. + * + * When setting a back button accessibility delegate, make sure to not dispatch any duplicate + * click events. Click events get injected in the internal accessibility delegate in + * {@link #setupBackButtonAccessibility(View, AccessibilityDelegate)}. */ public void setBackButtonAccessibilityDelegate(AccessibilityDelegate accessibilityDelegate) { if (mBackButton == null) { return; } - mBackButton.setAccessibilityDelegate(accessibilityDelegate); + boolean predictiveBackThreeButtonNav; + try { + predictiveBackThreeButtonNav = predictiveBackThreeButtonNav(); + } catch (Throwable t) { + predictiveBackThreeButtonNav = false; + } + if (predictiveBackThreeButtonNav) { + setupBackButtonAccessibility(mBackButton, accessibilityDelegate); + } else { + mBackButton.setAccessibilityDelegate(accessibilityDelegate); + } + } + + public void setWallpaperVisible(boolean isVisible) { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.setWallpaperVisibility(isVisible); + } + } + + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + mTransitionMode = barMode; + if (checkBarModes) { + checkNavBarModes(); + } + } + + public void checkNavBarModes() { + if (mContext.isPhoneMode()) { + boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0; + mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden); + } + } + + public void finishBarAnimations() { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.finishAnimations(); + } + } + + public void touchAutoDim(boolean reset) { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.setAutoDim(false); + mHandler.removeCallbacks(mAutoDim); + if (reset) { + mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS); + } + } + } + + public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) { + if (mContext.isPhoneMode()) { + mTaskbarTransitions.transitionTo(barMode, animate); + } } /** Use to set the translationY for the all nav+contextual buttons */ @@ -615,10 +788,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT return mTaskbarNavButtonTranslationY; } - /** - * Use to set the translationY for the all nav+contextual buttons when in - * Launcher - */ + /** Use to set the translationY for the all nav+contextual buttons when in Launcher */ public AnimatedFloat getTaskbarNavButtonTranslationYForInAppDisplay() { return mTaskbarNavButtonTranslationYForInAppDisplay; } @@ -628,9 +798,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT return mTaskbarNavButtonDarkIntensity; } - /** - * Use to override the nav button color with {@link #mOnBackgroundIconColor}. - */ + /** Use to override the nav button color with {@link #mOnBackgroundIconColor}. */ public AnimatedFloat getOnTaskbarBackgroundNavButtonColorOverride() { return mOnTaskbarBackgroundNavButtonColorOverride; } @@ -649,7 +817,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private void applyState() { int count = mPropertyHolders.size(); for (int i = 0; i < count; i++) { - mPropertyHolders.get(i).setState(mState); + mPropertyHolders.get(i).setState(mState, mContext.isGestureNav()); } } @@ -671,10 +839,10 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT final float normalTranslationY = mTaskbarNavButtonTranslationY.value; final float imeAdjustmentTranslationY = mTaskbarNavButtonTranslationYForIme.value; TaskbarUIController uiController = mControllers.uiController; - final float inAppDisplayAdjustmentTranslationY = (uiController instanceof LauncherTaskbarUIController - && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout()) - ? mTaskbarNavButtonTranslationYForInAppDisplay.value - : 0; + final float inAppDisplayAdjustmentTranslationY = + (uiController instanceof LauncherTaskbarUIController + && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout()) + ? mTaskbarNavButtonTranslationYForInAppDisplay.value : 0; mLastSetNavButtonTranslationY = normalTranslationY + imeAdjustmentTranslationY @@ -682,39 +850,78 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY); } + /** + * Sets Taskbar 3-button mode icon colors based on the + * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. For certain cases + * in large screen taskbar where there may be opaque surfaces, the selected SystemUI button + * colors are intentionally overridden. + *

+ * This method is also called when any of the AnimatedFloat instances change. + */ private void updateNavButtonColor() { final ArgbEvaluator argbEvaluator = ArgbEvaluator.getInstance(); - final int sysUiNavButtonIconColorOnHome = (int) argbEvaluator.evaluate( - mTaskbarNavButtonDarkIntensity.value, - mLightIconColorOnHome, - mDarkIconColorOnHome); + int taskbarNavButtonColor = getSysUiIconColorOnHome(argbEvaluator); + // Only phone mode foldable button colors should be identical to SysUI navbar colors. + if (!(ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode())) { + taskbarNavButtonColor = getTaskbarButtonColor(argbEvaluator, taskbarNavButtonColor); + } + applyButtonColors(taskbarNavButtonColor); + } - // Override the color from framework if nav buttons are over an opaque Taskbar - // surface. - final int iconColor = (int) argbEvaluator.evaluate( - mOnBackgroundNavButtonColorOverrideMultiplier.value - * Math.max( - mOnTaskbarBackgroundNavButtonColorOverride.value, - mSlideInViewVisibleNavButtonColorOverride.value), - sysUiNavButtonIconColorOnHome, + /** + * Taskbar 3-button mode icon colors based on the + * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. + */ + private int getSysUiIconColorOnHome(ArgbEvaluator argbEvaluator) { + return (int) argbEvaluator.evaluate(getTaskbarNavButtonDarkIntensity().value, + mLightIconColorOnWorkspace, mDarkIconColorOnWorkspace); + } + + /** + * If Taskbar background is opaque or slide in overlay is visible, the selected SystemUI button + * colors are intentionally overridden. The override can be disabled when + * {@link #mOnBackgroundNavButtonColorOverrideMultiplier} is {@code 0}. + */ + private int getTaskbarButtonColor(ArgbEvaluator argbEvaluator, int sysUiIconColorOnHome) { + final float sysUIColorOverride = + mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max( + mOnTaskbarBackgroundNavButtonColorOverride.value, + mSlideInViewVisibleNavButtonColorOverride.value); + return (int) argbEvaluator.evaluate(sysUIColorOverride, sysUiIconColorOnHome, mOnBackgroundIconColor); + } + /** + * Iteratively sets button colors for each button in {@link #mAllButtons}. + */ + private void applyButtonColors(int iconColor) { for (ImageView button : mAllButtons) { button.setImageTintList(ColorStateList.valueOf(iconColor)); Drawable background = button.getBackground(); if (background instanceof KeyButtonRipple) { ((KeyButtonRipple) background).setDarkIntensity( - mTaskbarNavButtonDarkIntensity.value); + getTaskbarNavButtonDarkIntensity().value); } } } + /** + * Updates Taskbar 3-Button icon colors as {@link #mTaskbarNavButtonDarkIntensity} changes. + */ + private void onDarkIntensityChanged() { + updateNavButtonColor(); + if (mContext.isPhoneMode()) { + mTaskbarTransitions.onDarkIntensityChanged(getTaskbarNavButtonDarkIntensity().value); + } + } + protected ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) { return addButton(drawableId, buttonType, parent, navButtonController, id, R.layout.taskbar_nav_button); } + @SuppressLint("ClickableViewAccessibility") private ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id, @LayoutRes int layoutId) { @@ -722,11 +929,103 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT buttonView.setImageResource(drawableId); buttonView.setContentDescription(parent.getContext().getString( navButtonController.getButtonContentDescription(buttonType))); - buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType, view)); - buttonView.setOnLongClickListener(view -> navButtonController.onButtonLongClick(buttonType, view)); + boolean predictiveBackThreeButtonNav; + try { + predictiveBackThreeButtonNav = predictiveBackThreeButtonNav(); + } catch (Throwable t) { + predictiveBackThreeButtonNav = false; + } + if (predictiveBackThreeButtonNav && buttonType == BUTTON_BACK) { + // set up special touch listener for back button to support predictive back + setupBackButtonAccessibility(buttonView, null); + setBackButtonTouchListener(buttonView, navButtonController); + // Set this View clickable, so that NearestTouchFrame.java forwards closeby touches to + // this View + buttonView.setClickable(true); + } else { + buttonView.setOnClickListener(view -> + navButtonController.onButtonClick(buttonType, view)); + buttonView.setOnLongClickListener(view -> + navButtonController.onButtonLongClick(buttonType, view)); + buttonView.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + buttonView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + return false; + }); + } return buttonView; } + private void setupBackButtonAccessibility(View backButton, + AccessibilityDelegate accessibilityDelegate) { + View.AccessibilityDelegate backButtonAccessibilityDelegate = + new View.AccessibilityDelegate() { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (accessibilityDelegate != null) { + accessibilityDelegate.performAccessibilityAction(host, action, args); + } + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + mControllers.navButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, + /*cancelled*/ false); + mControllers.navButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, + /*cancelled*/ false); + return true; + } + return super.performAccessibilityAction(host, action, args); + } + }; + backButton.setAccessibilityDelegate(backButtonAccessibilityDelegate); + } + + private void setBackButtonTouchListener(View buttonView, + TaskbarNavButtonController navButtonController) { + final RectF rect = new RectF(); + final AtomicBoolean hasSentDownEvent = new AtomicBoolean(false); + final Runnable longPressTimeout = () -> { + navButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, /*cancelled*/ false); + hasSentDownEvent.set(true); + }; + buttonView.setOnTouchListener((v, event) -> { + int motionEventAction = event.getAction(); + if (motionEventAction == MotionEvent.ACTION_DOWN) { + hasSentDownEvent.set(false); + mHandler.postDelayed(longPressTimeout, PREDICTIVE_BACK_TIMEOUT_MS); + rect.set(0, 0, v.getWidth(), v.getHeight()); + buttonView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + boolean isCancelled = motionEventAction == MotionEvent.ACTION_CANCEL + || (!rect.contains(event.getX(), event.getY()) + && (motionEventAction == MotionEvent.ACTION_MOVE + || motionEventAction == MotionEvent.ACTION_UP)); + if (motionEventAction != MotionEvent.ACTION_UP && !isCancelled) { + // return early. we don't care about any other cases than UP or CANCEL from here on + return false; + } + mHandler.removeCallbacks(longPressTimeout); + if (!hasSentDownEvent.get()) { + if (isCancelled) { + // if it is cancelled and ACTION_DOWN has not been sent yet, return early and + // don't send anything to sysui. + return false; + } + navButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, isCancelled); + } + navButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, isCancelled); + if (motionEventAction == MotionEvent.ACTION_UP && !isCancelled) { + buttonView.performClick(); + } + return false; + }); + buttonView.setOnLongClickListener((view) -> { + navButtonController.onButtonLongClick(BUTTON_BACK, view); + return false; + }); + } + private ImageView addButton(ViewGroup parent, @IdRes int id, @LayoutRes int layoutId) { ImageView buttonView = (ImageView) mContext.getLayoutInflater() .inflate(layoutId, parent, false); @@ -744,19 +1043,23 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT if (mFloatingRotationButton != null) { mFloatingRotationButton.onConfigurationChanged(configChanges); } - if (!mContext.isUserSetupComplete()) { + if (!mContext.isUserSetupComplete() && !ENABLE_TASKBAR_NAVBAR_UNIFICATION) { handleSetupUi(); } updateButtonLayoutSpacing(); } private void handleSetupUi() { + // Setup wizard handles the UI when the expressive theme is enabled and Simple View isn't. + if (mIsExpressiveThemeEnabled && !mContext.isSimpleViewEnabled()) { + return; + } // Since setup wizard only has back button enabled, it looks strange to be // end-aligned, so start-align instead. - FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) mNavButtonContainer - .getLayoutParams(); - FrameLayout.LayoutParams navButtonsViewLayoutParams = (FrameLayout.LayoutParams) mNavButtonsView - .getLayoutParams(); + FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) + mNavButtonContainer.getLayoutParams(); + FrameLayout.LayoutParams navButtonsViewLayoutParams = (FrameLayout.LayoutParams) + mNavButtonsView.getLayoutParams(); Resources resources = mContext.getResources(); DeviceProfile deviceProfile = mContext.getDeviceProfile(); @@ -767,9 +1070,9 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT // If SUW is on a large screen device that is landscape (or has a square aspect // ratio) the back button has to be placed accordingly - if ((deviceProfile.isTablet && deviceProfile.isLandscape) - || (deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND - && deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND)) { + if ((deviceProfile.getDeviceProperties().isTablet() && deviceProfile.getDeviceProperties().isLandscape()) + || (deviceProfile.getDeviceProperties().getAspectRatio() > SQUARE_ASPECT_RATIO_BOTTOM_BOUND + && deviceProfile.getDeviceProperties().getAspectRatio() < SQUARE_ASPECT_RATIO_UPPER_BOUND)) { navButtonsLayoutParams.setMarginStart( resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_start_margin)); navButtonsViewLayoutParams.bottomMargin = resources.getDimensionPixelSize( @@ -780,7 +1083,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT int phoneOrPortraitSetupMargin = resources.getDimensionPixelSize( R.dimen.taskbar_contextual_button_suw_margin); navButtonsLayoutParams.setMarginStart(phoneOrPortraitSetupMargin); - navButtonsLayoutParams.bottomMargin = !deviceProfile.isLandscape + navButtonsLayoutParams.bottomMargin = !deviceProfile.getDeviceProperties().isLandscape() ? 0 : phoneOrPortraitSetupMargin - (resources.getDimensionPixelSize( R.dimen.taskbar_nav_buttons_size) / 2); @@ -792,8 +1095,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } /** - * Adds the correct spacing to 3 button nav container depending on if device is - * in kids mode, + * Adds the correct spacing to 3 button nav container depending on if device is in kids mode, * setup wizard, or normal 3 button nav. */ private void updateButtonLayoutSpacing() { @@ -806,10 +1108,11 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT boolean isInKidsMode = mContext.isNavBarKidsModeActive(); if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { - NavButtonLayoutter navButtonLayoutter = NavButtonLayoutFactory.Companion.getUiLayoutter( - dp, mNavButtonsView, mImeSwitcherButton, - mA11yButton, mSpace, res, isInKidsMode, isInSetup, isThreeButtonNav, - mContext.isPhoneMode(), mWindowManagerProxy.getRotation(mContext)); + NavButtonLayoutter navButtonLayoutter = + NavButtonLayoutFactory.Companion.getUiLayoutter( + dp, mNavButtonsView, mImeSwitcherButton, + mA11yButton, mSpace, res, isInKidsMode, isInSetup, isThreeButtonNav, + mContext.isPhoneMode(), mWindowManagerProxy.getRotation(mContext)); navButtonLayoutter.layoutButtons(mContext, isA11yButtonPersistent()); updateButtonsBackground(); updateNavButtonColor(); @@ -849,7 +1152,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT // Home button layout LinearLayout.LayoutParams homeLayoutparams = new LinearLayout.LayoutParams( buttonWidth, - buttonHeight); + buttonHeight + ); int homeButtonLeftMargin = res.getDimensionPixelSize( R.dimen.taskbar_home_button_left_margin_kids); homeLayoutparams.setMargins(homeButtonLeftMargin, 0, 0, 0); @@ -858,7 +1162,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT // Back button layout LinearLayout.LayoutParams backLayoutParams = new LinearLayout.LayoutParams( buttonWidth, - buttonHeight); + buttonHeight + ); int backButtonLeftMargin = res.getDimensionPixelSize( R.dimen.taskbar_back_button_left_margin_kids); backLayoutParams.setMargins(backButtonLeftMargin, 0, 0, 0); @@ -872,8 +1177,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mBackButton.setBackground(buttonBackground); // Update alignment within taskbar - FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) mNavButtonContainer - .getLayoutParams(); + FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) + mNavButtonContainer.getLayoutParams(); navButtonsLayoutParams.setMarginStart( navButtonsLayoutParams.getMarginEnd() / 2); navButtonsLayoutParams.setMarginEnd(navButtonsLayoutParams.getMarginStart()); @@ -890,15 +1195,15 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT // Setup normal 3 button // Add spacing after the end of the last nav button - FrameLayout.LayoutParams navButtonParams = (FrameLayout.LayoutParams) mNavButtonContainer.getLayoutParams(); + FrameLayout.LayoutParams navButtonParams = + (FrameLayout.LayoutParams) mNavButtonContainer.getLayoutParams(); navButtonParams.gravity = Gravity.END; navButtonParams.width = FrameLayout.LayoutParams.WRAP_CONTENT; navButtonParams.height = MATCH_PARENT; int navMarginEnd = (int) res.getDimension(dp.inv.inlineNavButtonsEndSpacing); int contextualWidth = mEndContextualContainer.getWidth(); - // If contextual buttons are showing, we check if the end margin is enough for - // the + // If contextual buttons are showing, we check if the end margin is enough for the // contextual button to be showing - if not, move the nav buttons over a smidge if (isA11yButtonPersistent() && navMarginEnd < contextualWidth) { // Additional spacing, eat up half of space between last icon and nav button @@ -911,7 +1216,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT int spaceInBetween = res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween); for (int i = 0; i < mNavButtonContainer.getChildCount(); i++) { View navButton = mNavButtonContainer.getChildAt(i); - LinearLayout.LayoutParams buttonLayoutParams = (LinearLayout.LayoutParams) navButton.getLayoutParams(); + LinearLayout.LayoutParams buttonLayoutParams = + (LinearLayout.LayoutParams) navButton.getLayoutParams(); buttonLayoutParams.weight = 0; if (i == 0) { buttonLayoutParams.setMarginEnd(spaceInBetween / 2); @@ -948,9 +1254,9 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT public void onDestroy() { mPropertyHolders.clear(); - mControllers.rotationButtonController.unregisterListeners(); if (mFloatingRotationButton != null) { mFloatingRotationButton.hide(); + mFloatingRotationButton = null; } moveNavButtonsBackToTaskbarWindow(); @@ -961,8 +1267,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } /** - * Moves mNavButtonsView from TaskbarDragLayer to a placeholder BaseDragLayer on - * a new window. + * Moves mNavButtonsView from TaskbarDragLayer to a placeholder BaseDragLayer on a new window. */ public void moveNavButtonsToNewWindow() { if (mAreNavButtonsInSeparateWindow) { @@ -970,8 +1275,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } if (mIsImeRenderingNavButtons) { - // IME is rendering the nav buttons, so we don't need to create a new layer for - // them. + // IME is rendering the nav buttons, so we don't need to create a new layer for them. return; } @@ -1000,8 +1304,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } /** - * Moves mNavButtonsView from its temporary window and reattaches it to - * TaskbarDragLayer. + * Moves mNavButtonsView from its temporary window and reattaches it to TaskbarDragLayer. */ public void moveNavButtonsBackToTaskbarWindow() { if (!mAreNavButtonsInSeparateWindow) { @@ -1020,8 +1323,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } /** - * Called whenever a new ui controller is set, and should update anything that - * depends on the + * Called whenever a new ui controller is set, and should update anything that depends on the * ui controller. */ public void onUiControllerChanged() { @@ -1054,13 +1356,17 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT + mOnBackgroundNavButtonColorOverrideMultiplier.value); mNavButtonsView.dumpLogs(prefix + "\t", pw); + if (mContext.isPhoneMode()) { + mTaskbarTransitions.dumpLogs(prefix + "\t", pw); + } } private static String getStateString(int flags) { StringJoiner str = new StringJoiner("|"); - appendFlag(str, flags, FLAG_SWITCHER_SHOWING, "FLAG_SWITCHER_SHOWING"); + appendFlag(str, flags, FLAG_IME_SWITCHER_BUTTON_VISIBLE, + "FLAG_IME_SWITCHER_BUTTON_VISIBLE"); appendFlag(str, flags, FLAG_IME_VISIBLE, "FLAG_IME_VISIBLE"); - appendFlag(str, flags, FLAG_ROTATION_BUTTON_VISIBLE, "FLAG_ROTATION_BUTTON_VISIBLE"); + appendFlag(str, flags, FLAG_BACK_DISMISS_IME, "FLAG_BACK_DISMISS_IME"); appendFlag(str, flags, FLAG_A11Y_VISIBLE, "FLAG_A11Y_VISIBLE"); appendFlag(str, flags, FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE, "FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE"); @@ -1090,6 +1396,105 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mHitboxExtender.onAnimationProgressToOverview(alignment); } + /** Adjusts navigation buttons layout accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + boolean locationUpdated = location != mBubbleBarTargetLocation; + if (locationUpdated) { + cancelExistingNavBarAnimation(); + } else { + endExistingAnimation(); + } + mNavButtonContainer.setTranslationX(getNavBarTranslationX(location)); + mBubbleBarTargetLocation = location; + } + + /** Animates navigation buttons accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationAnimated(BubbleBarLocation location) { + if (location == mBubbleBarTargetLocation) return; + cancelExistingNavBarAnimation(); + mBubbleBarTargetLocation = location; + int finalX = getNavBarTranslationX(location); + Animator teleportAnimator = BarsLocationAnimatorHelper + .getTeleportAnimatorForNavButtons(location, mNavButtonContainer, finalX); + teleportAnimator.addListener(forEndCallback(() -> mNavBarLocationAnimator = null)); + mNavBarLocationAnimator = teleportAnimator; + mNavBarLocationAnimator.start(); + } + + private void endExistingAnimation() { + if (mNavBarLocationAnimator != null) { + mNavBarLocationAnimator.end(); + mNavBarLocationAnimator = null; + } + } + + private void cancelExistingNavBarAnimation() { + if (mNavBarLocationAnimator != null) { + mNavBarLocationAnimator.cancel(); + mNavBarLocationAnimator = null; + } + } + + private int getNavBarTranslationX(BubbleBarLocation location) { + boolean isNavbarOnRight = location.isOnLeft(mNavButtonsView.isLayoutRtl()); + DeviceProfile dp = mContext.getDeviceProfile(); + float navBarTargetStartX; + if (!mContext.isUserSetupComplete()) { + // Skip additional translations on the nav bar container while in SUW layout + return 0; + } else if (mContext.shouldStartAlignTaskbar()) { + int navBarSpacing = dp.getHotseatProfile().getInlineNavButtonsEndSpacingPx(); + // If the taskbar is start aligned the navigation bar is aligned to the start or end of + // the container, depending on the bubble bar location + if (isNavbarOnRight) { + navBarTargetStartX = dp.getDeviceProperties().getWidthPx() - navBarSpacing - mNavButtonContainer.getWidth(); + } else { + navBarTargetStartX = navBarSpacing; + } + } else { + // If the task bar is not start aligned, the navigation bar is located in the center + // between the taskbar and screen edges, depending on the bubble bar location. + float navbarWidth = mNavButtonContainer.getWidth(); + Rect taskbarBounds = mControllers.taskbarViewController + .getTransientTaskbarIconLayoutBoundsInParent(); + if (isNavbarOnRight) { + if (mNavButtonsView.isLayoutRtl()) { + float taskBarEnd = taskbarBounds.right; + navBarTargetStartX = (dp.getDeviceProperties().getWidthPx() + taskBarEnd - navbarWidth) / 2; + } else { + navBarTargetStartX = mNavButtonContainer.getLeft(); + } + } else { + float taskBarStart = taskbarBounds.left; + navBarTargetStartX = (taskBarStart - navbarWidth) / 2; + } + } + return (int) navBarTargetStartX - mNavButtonContainer.getLeft(); + } + + /** Adjusts the navigation buttons layout position according to the bubble bar location. */ + public void onLayoutsUpdated() { + // no need to do anything if on phone, or if taskbar or navbar views were not placed on + // screen. + Rect transientTaskbarIconLayoutBoundsInParent = mControllers.taskbarViewController + .getTransientTaskbarIconLayoutBoundsInParent(); + if (mContext.getDeviceProfile().getDeviceProperties().isPhone() + || transientTaskbarIconLayoutBoundsInParent.isEmpty() + || mNavButtonsView.getWidth() == 0) { + return; + } + if (mControllers.bubbleControllers.isPresent()) { + if (mBubbleBarTargetLocation == null) { + // only set bubble bar location if it was not set before + mBubbleBarTargetLocation = mControllers.bubbleControllers.get() + .bubbleBarViewController.getBubbleBarLocation(); + } + onBubbleBarLocationUpdated(mBubbleBarTargetLocation); + } + } + private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback { @Override public void onVisibilityChanged(boolean isVisible) { @@ -1102,83 +1507,6 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT } } - private class RotationButtonImpl implements RotationButton { - - private final ImageView mButton; - private AnimatedVectorDrawable mImageDrawable; - - RotationButtonImpl(ImageView button) { - mButton = button; - } - - @Override - public void setRotationButtonController(RotationButtonController rotationButtonController) { - // TODO(b/187754252) UI polish, different icons based on light/dark context, etc - mImageDrawable = (AnimatedVectorDrawable) mButton.getContext() - .getDrawable(rotationButtonController.getIconResId()); - mButton.setImageDrawable(mImageDrawable); - mButton.setContentDescription(mButton.getResources() - .getString(R.string.accessibility_rotate_button)); - mImageDrawable.setCallback(mButton); - } - - @Override - public View getCurrentView() { - return mButton; - } - - @Override - public boolean show() { - mButton.setVisibility(View.VISIBLE); - mState |= FLAG_ROTATION_BUTTON_VISIBLE; - applyState(); - return true; - } - - @Override - public boolean hide() { - mButton.setVisibility(View.GONE); - mState &= ~FLAG_ROTATION_BUTTON_VISIBLE; - applyState(); - return true; - } - - @Override - public boolean isVisible() { - return mButton.getVisibility() == View.VISIBLE; - } - - @Override - public void updateIcon(int lightIconColor, int darkIconColor) { - // TODO(b/187754252): UI Polish - } - - @Override - public void setOnClickListener(OnClickListener onClickListener) { - mButton.setOnClickListener(onClickListener); - } - - @Override - public void setOnHoverListener(OnHoverListener onHoverListener) { - mButton.setOnHoverListener(onHoverListener); - } - - @Override - public AnimatedVectorDrawable getImageDrawable() { - return mImageDrawable; - } - - @Override - public void setDarkIntensity(float darkIntensity) { - // TODO(b/187754252) UI polish - } - - @Override - public boolean acceptRotationProposal() { - return mButton.isAttachedToWindow(); - } - } - private static class StatePropertyHolder { private final float mEnabledValue, mDisabledValue; @@ -1209,13 +1537,16 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue); } - public void setState(int flags) { + public void setState(int flags, boolean skipAnimation) { boolean isEnabled = mEnableCondition.test(flags); if (mIsEnabled != isEnabled) { mIsEnabled = isEnabled; mAnimator.cancel(); mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue); mAnimator.start(); + if (skipAnimation) { + mAnimator.end(); + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.kt new file mode 100644 index 0000000000..dc66e0b7ca --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/NewWindowTaskbarShortcut.kt @@ -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. + */ + +package com.android.launcher3.taskbar + +import android.content.Context +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.view.View +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.R +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.views.ActivityContext + +/** + * A single menu item shortcut to execute creating a new instance of an app. Default interaction for + * [onClick] is to launch the app in full screen or as a floating window in Desktop Mode. + */ +class NewWindowTaskbarShortcut(target: T, itemInfo: ItemInfo?, originalView: View?) : + SystemShortcut( + R.drawable.desktop_mode_ic_taskbar_menu_new_window, + R.string.new_window_option_taskbar, + target, + itemInfo, + originalView + ) where T : Context?, T : ActivityContext? { + + override fun onClick(v: View?) { + val intent = mItemInfo.intent ?: return + intent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + mTarget?.startActivitySafely(v, intent, mItemInfo) + AbstractFloatingView.closeAllOpenViews(mTarget) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/NudgeView.java b/quickstep/src/com/android/launcher3/taskbar/NudgeView.java new file mode 100644 index 0000000000..543475935e --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/NudgeView.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import static com.android.launcher3.anim.AnimatedFloat.VALUE; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; +import static com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_NUDGED; +import static com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED; + +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.Nullable; + +import com.android.launcher3.Flags; +import com.android.launcher3.R; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.SpringAnimationBuilder; +import com.android.launcher3.util.MultiValueAlpha; + +public class NudgeView extends ImageView { + + private static final float DAMPING_RATIO = 0.6f; + private static final float STIFFNESS = 380f; + + private AnimatorSet mAnimatorSet; + private boolean mNudgeShown; + private int mNudgePillWidth; + private int mNudgePillHeight; + private int mStashedHandleWidth; + private int mStashedHandleHeight; + private ImageView mNudgeIcon; + private MultiValueAlpha mStashedHandleViewAlpha; + + private final AnimatedFloat mAlphaForHandleView = new AnimatedFloat( + this::updateAlphaForHandleView); + private final AnimatedFloat mAlphaForNudgeIcon = new AnimatedFloat( + this::updateAlphaForNudgeIcon); + private final AnimatedFloat mWidthForNudgePill = new AnimatedFloat( + this::updateWidthForNudge); + private final AnimatedFloat mHeightForNudgePill = new AnimatedFloat( + this::updateHeightForNudge); + + public NudgeView(Context context) { + this(context, null); + } + + public NudgeView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NudgeView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public NudgeView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + if (Flags.nudgePill()) { + mNudgePillWidth = context.getResources().getDimensionPixelSize( + R.dimen.taskbar_nudge_pill_width); + mNudgePillHeight = context.getResources().getDimensionPixelSize( + R.dimen.taskbar_nudge_pill_height); + mStashedHandleWidth = context.getResources().getDimensionPixelSize( + R.dimen.taskbar_nudge_pill_width); + mStashedHandleHeight = context.getResources().getDimensionPixelSize( + R.dimen.taskbar_stashed_handle_height); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mNudgeIcon = findViewById(R.id.nudge_icon); + } + + void updateNudgeIcon(boolean showNudge, @Nullable Drawable icon, + @Nullable MultiValueAlpha stashedHandleViewAlpha) { + if (!Flags.nudgePill() || stashedHandleViewAlpha == null) { + return; + } + mStashedHandleViewAlpha = stashedHandleViewAlpha; + // Set update visibility to false when showing the nudge otherwise + // StashHandleView will be INVISIBLE at low alpha resulting in being unable to invoke CtS. + mStashedHandleViewAlpha.setUpdateVisibility(false); + mNudgeIcon.setImageDrawable(null); + if (icon != null) { + mNudgeIcon.setImageDrawable(icon); + } + mNudgeIcon.setVisibility(VISIBLE); + if (showNudge && !mNudgeShown) { + post(() -> animationBarToPill(true /* showNudge */)); + } else if (!showNudge){ + post(() -> animationBarToPill(false /* showNudge */)); + } + } + + private void animationBarToPill(boolean showNudge) { + if (!Flags.nudgePill()) { + return; + } + if (mAnimatorSet != null && mAnimatorSet.isRunning()) { + mAnimatorSet.cancel(); + } + mNudgeShown = showNudge; + // Alpha + float targetIconAlpha = showNudge ? 1.0f : 0; + float targetHandleViewAlpha = showNudge ? 0 : 1.0f; + // Animate from navBar -> nudgePill if showNudge is true. + float startWidth = showNudge ? mStashedHandleWidth : mNudgePillWidth; + float startHeight = showNudge ? mStashedHandleHeight : mNudgePillHeight; + float endWidth = showNudge ? mNudgePillWidth : mStashedHandleWidth; + float endHeight = showNudge ? mNudgePillHeight : mStashedHandleHeight; + + ValueAnimator alphaNudgeIcon = new SpringAnimationBuilder(mContext) + .setStartValue(mNudgeIcon.getAlpha()) + .setEndValue(targetIconAlpha) + .setDampingRatio(DAMPING_RATIO) + .setStiffness(STIFFNESS) + .build(mAlphaForNudgeIcon, VALUE); + if (!showNudge) { + alphaNudgeIcon.addListener(forEndCallback(() -> { + mNudgeIcon.setVisibility(GONE); + mStashedHandleViewAlpha.setUpdateVisibility(true); + })); + } + ValueAnimator alphaHandleView = new SpringAnimationBuilder(mContext) + .setStartValue(mStashedHandleViewAlpha.get(ALPHA_INDEX_STASHED).getValue()) + .setEndValue(targetHandleViewAlpha) + .setDampingRatio(DAMPING_RATIO) + .setStiffness(STIFFNESS) + .build(mAlphaForHandleView, VALUE); + ValueAnimator widthAnimation = new SpringAnimationBuilder(mContext) + .setStartValue(startWidth) + .setEndValue(endWidth) + .setDampingRatio(DAMPING_RATIO) + .setStiffness(STIFFNESS) + .build(mWidthForNudgePill, VALUE); + ValueAnimator heightAnimation = new SpringAnimationBuilder(mContext) + .setStartValue(startHeight) + .setEndValue(endHeight) + .setDampingRatio(DAMPING_RATIO) + .setStiffness(STIFFNESS) + .build(mHeightForNudgePill, VALUE); + mAnimatorSet = new AnimatorSet(); + mAnimatorSet.playTogether( + alphaNudgeIcon, alphaHandleView, + widthAnimation, heightAnimation + ); + mAnimatorSet.start(); + } + + private void updateAlphaForHandleView() { + if (!Flags.nudgePill()) { + return; + } + float alpha = mAlphaForHandleView.value; + mStashedHandleViewAlpha.get(ALPHA_INDEX_NUDGED).setValue(alpha); + } + + private void updateAlphaForNudgeIcon() { + if (!Flags.nudgePill()) { + return; + } + float alpha = mAlphaForNudgeIcon.value; + mNudgeIcon.setAlpha(alpha); + } + + private void updateWidthForNudge() { + if (!Flags.nudgePill()) { + return; + } + float width = mWidthForNudgePill.value; + ViewGroup.MarginLayoutParams nudgePillLayoutParams = + (ViewGroup.MarginLayoutParams) mNudgeIcon.getLayoutParams(); + nudgePillLayoutParams.width = (int) width; + mNudgeIcon.setLayoutParams(nudgePillLayoutParams); + } + + private void updateHeightForNudge() { + if (!Flags.nudgePill()) { + return; + } + float height = mHeightForNudgePill.value; + ViewGroup.MarginLayoutParams nudgePillLayoutParams = + (ViewGroup.MarginLayoutParams) mNudgeIcon.getLayoutParams(); + nudgePillLayoutParams.height = (int) height; + mNudgeIcon.setLayoutParams(nudgePillLayoutParams); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/NudgeViewController.java b/quickstep/src/com/android/launcher3/taskbar/NudgeViewController.java new file mode 100644 index 0000000000..89d4ec4476 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/NudgeViewController.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar; + +import static android.content.Context.RECEIVER_EXPORTED; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.android.launcher3.Flags; +import com.android.launcher3.R; +import com.android.launcher3.util.SimpleBroadcastReceiver; + +import java.io.PrintWriter; + +/** + * NudgeViewController handles the broadcasts sent to launcher which will then update the view. + */ +public class NudgeViewController implements TaskbarControllers.LoggableTaskbarController{ + + private static final String NAV_UPDATE_ACTION = "android.app.action.UPDATE_NAVBAR"; + private static final String GAME = "game"; + private static final String TRANSLATE = "translate"; + private static final String SHOW_NUDGE = "showNudge"; + private static final String NUDGE_ICON = "nudgeIcon"; + private final TaskbarActivityContext mActivity; + @Nullable + private final NudgeView mNudgeView; + private final Drawable mTranslateIcon; + private final Drawable mGameIcon; + + @Nullable + private SimpleBroadcastReceiver mNudgeReceiver; + + public NudgeViewController(TaskbarActivityContext activity, + @Nullable NudgeView nudgeView) { + mActivity = activity; + mNudgeView = nudgeView; + final Resources resources = mActivity.getResources(); + if (Flags.nudgePill() && mNudgeView != null) { + mNudgeReceiver = new SimpleBroadcastReceiver( + mActivity, UI_HELPER_EXECUTOR, this::shouldChangeNavBar); + mNudgeReceiver.register(RECEIVER_EXPORTED, NAV_UPDATE_ACTION); + } + mTranslateIcon = resources.getDrawable(R.drawable.ic_translate); + mGameIcon = resources.getDrawable(R.drawable.ic_game); + } + + private void shouldChangeNavBar(Intent i) { + if (!mActivity.isPhoneGestureNavMode() || !Flags.nudgePill()) { + return; + } + Bundle bundle = i.getExtras(); + boolean showNudge = bundle.getBoolean(SHOW_NUDGE, false); + String iconToUse = bundle.getString(NUDGE_ICON, ""); + Drawable icon = null; + if (GAME.equals(iconToUse)) { + icon = mGameIcon; + } else if (TRANSLATE.equals(iconToUse)) { + icon = mTranslateIcon; + } + // Nudge icon will only show if there is a valid icon to show. + mNudgeView.updateNudgeIcon(icon != null && showNudge /* showNudge */, icon, + mActivity.getControllers().stashedHandleViewController.getStashedHandleAlpha()); + } + + public void onDestroy() { + if (mNudgeReceiver != null) { + mNudgeReceiver.unregisterReceiverSafely(); + } + mNudgeReceiver = null; + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "NudgeViewController:"); + pw.println(prefix + "\tmNudgeView=" + mNudgeView); + pw.println(prefix + "\tmNudgeReceiver=" + mNudgeReceiver); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt new file mode 100644 index 0000000000..0dce63a838 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.content.Context +import android.util.SparseArray +import android.view.View +import android.window.DesktopExperienceFlags +import androidx.annotation.VisibleForTesting +import com.android.launcher3.DeviceProfile +import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.R +import com.android.launcher3.model.BgDataModel +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.views.ActivityContext + +/** + * A single menu item shortcut to allow users to pin an item to the taskbar and unpin an item from + * the taskbar. + */ +class PinToTaskbarShortcut( + target: T, + itemInfo: ItemInfo?, + originalView: View, + @get:VisibleForTesting val mIsPin: Boolean, + private val mPinnedInfoList: SparseArray, +) : + SystemShortcut( + if (mIsPin) R.drawable.ic_pin else R.drawable.ic_unpin, + if (mIsPin) R.string.pin_to_taskbar else R.string.unpin_from_taskbar, + target, + itemInfo, + originalView, + ) where T : Context?, T : ActivityContext? { + + override fun onClick(v: View?) { + dismissTaskMenuView() + // Create a placeholder callbacks for the writer to notify other launcher model callbacks + // after update. + val callbacks: BgDataModel.Callbacks = object : BgDataModel.Callbacks {} + + val writer = + LauncherAppState.getInstance(mOriginalView.context) + .model + .getWriter(true, mTarget!!.cellPosMapper, callbacks) + + if (!mIsPin) { + var infoToUnpin = mItemInfo + if (mItemInfo.container == CONTAINER_ALL_APPS) { + for (i in 0.. STASHED_HANDLE_REGION_IS_DARK = + nonRestorableItem(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, false, ENCRYPTED); + private final TaskbarActivityContext mActivity; - private final SharedPreferences mPrefs; + private final LauncherPrefs mPrefs; private final StashedHandleView mStashedHandleView; private int mStashedHandleWidth; private final int mStashedHandleHeight; @@ -94,6 +100,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT // States that affect whether region sampling is enabled or not private boolean mIsStashed; private boolean mIsLumaSamplingEnabled; + private boolean mIsAppTransitionPending; private boolean mTaskbarHidden; private float mTranslationYForSwipe; @@ -102,13 +109,12 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT public StashedHandleViewController(TaskbarActivityContext activity, StashedHandleView stashedHandleView) { mActivity = activity; - mPrefs = LauncherPrefs.getPrefs(mActivity); + mPrefs = LauncherPrefs.get(mActivity); mStashedHandleView = stashedHandleView; mTaskbarStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, NUM_ALPHA_CHANNELS); mTaskbarStashedHandleAlpha.setUpdateVisibility(true); mStashedHandleView.updateHandleColor( - mPrefs.getBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, false), - false /* animate */); + mPrefs.get(STASHED_HANDLE_REGION_IS_DARK), false /* animate */); final Resources resources = mActivity.getResources(); mStashedHandleHeight = resources.getDimensionPixelSize( R.dimen.taskbar_stashed_handle_height); @@ -118,16 +124,17 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT mControllers = controllers; DeviceProfile deviceProfile = mActivity.getDeviceProfile(); Resources resources = mActivity.getResources(); - if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()) { + if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar() + || mActivity.isBubbleBarOnPhone()) { mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size); mStashedHandleWidth = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen); } else { - mTaskbarSize = deviceProfile.taskbarHeight; + mTaskbarSize = deviceProfile.getTaskbarProfile().getHeight(); mStashedHandleWidth = resources .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width); } - int taskbarBottomMargin = deviceProfile.taskbarBottomMargin; + int taskbarBottomMargin = deviceProfile.getTaskbarProfile().getBottomMargin(); mStashedHandleView.getLayoutParams().height = mTaskbarSize + taskbarBottomMargin; mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_STASHED).setValue( @@ -146,7 +153,9 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT stashedCenterX + mStashedHandleWidth / 2, stashedCenterY + mStashedHandleHeight / 2); mStashedHandleView.updateSampledRegion(mStashedHandleBounds); - mStashedHandleRadius = view.getHeight() / 2f; + mStashedHandleRadius = Flags.enableLauncherIconShapes() + ? getShapedTaskbarRadius(mActivity) + : view.getHeight() / 2f; outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius); } }); @@ -166,6 +175,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT /** * Returns the stashed handle bounds. + * * @param out The destination rect. */ public void getStashedHandleBounds(Rect out) { @@ -178,8 +188,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT @Override public void onRegionDarknessChanged(boolean isRegionDark) { mStashedHandleView.updateHandleColor(isRegionDark, true /* animate */); - mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, - isRegionDark).apply(); + mPrefs.put(STASHED_HANDLE_REGION_IS_DARK, isRegionDark); } @Override @@ -191,11 +200,13 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT public void onDestroy() { - mRegionSamplingHelper.stopAndDestroy(); + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.stopAndDestroy(); + } mRegionSamplingHelper = null; } - public MultiPropertyFactory getStashedHandleAlpha() { + public MultiValueAlpha getStashedHandleAlpha() { return mTaskbarStashedHandleAlpha; } @@ -209,16 +220,18 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT * morphs into the size of where the taskbar icons will be. */ public Animator createRevealAnimToIsStashed(boolean isStashed) { - Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds()); + Rect visualBounds = mControllers.taskbarViewController + .getTransientTaskbarIconLayoutBounds(); float startRadius = mStashedHandleRadius; - if (DisplayController.isTransientTaskbar(mActivity)) { + if (mActivity.isTransientTaskbar()) { // Account for the full visual height of the transient taskbar. int heightDiff = (mTaskbarSize - visualBounds.height()) / 2; visualBounds.top -= heightDiff; visualBounds.bottom += heightDiff; - - startRadius = visualBounds.height() / 2f; + startRadius = Flags.enableLauncherIconShapes() + ? getShapedTaskbarRadius(mActivity) + : visualBounds.height() / 2f; } final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider( @@ -257,6 +270,11 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT updateSamplingState(); } + public void setIsAppTransitionPending(boolean pending) { + mIsAppTransitionPending = pending; + updateSamplingState(); + } + private void updateSamplingState() { updateRegionSamplingWindowVisibility(); if (shouldSample()) { @@ -268,7 +286,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT } private boolean shouldSample() { - return mIsStashed && mIsLumaSamplingEnabled; + return mIsStashed && mIsLumaSamplingEnabled && !mIsAppTransitionPending; } protected void updateStashedHandleHintScale() { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 4e8034da63..f7e760d44e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -16,15 +16,19 @@ package com.android.launcher3.taskbar; import static android.os.Trace.TRACE_TAG_APP; -import static android.view.Display.DEFAULT_DISPLAY; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED; +import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE; + +import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP; import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY; import static com.android.launcher3.Flags.enableCursorHoverStates; @@ -36,14 +40,18 @@ import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN; import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING; import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN; +import static com.android.launcher3.taskbar.TaskbarStashController.SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE; import static com.android.launcher3.testing.shared.ResourceUtils.getBoolByName; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback; +import static com.android.quickstep.util.ExternalDisplaysKt.isExternalDisplay; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; +import static com.android.wm.shell.Flags.enableBubbleBar; +import static com.android.wm.shell.Flags.enableBubbleBarOnPhones; import static com.android.wm.shell.Flags.enableTinyTaskbar; -import static java.util.stream.Collectors.toList; +import static java.lang.invoke.MethodHandles.Lookup.PROTECTED; import android.animation.AnimatorSet; import android.animation.ValueAnimator; @@ -55,20 +63,26 @@ import android.content.pm.ActivityInfo.Config; import android.content.pm.LauncherApps; import android.content.res.Resources; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; import android.os.IRemoteCallback; import android.os.Process; import android.os.Trace; import android.provider.Settings; import android.util.Log; -import android.view.Display; import android.view.Gravity; import android.view.Surface; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; +import android.widget.FrameLayout; import android.widget.Toast; +import android.window.DesktopExperienceFlags; +import android.window.DesktopModeFlags; +import android.window.DesktopModeFlags.DesktopModeFlag; +import android.window.RemoteTransition; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -76,42 +90,64 @@ import androidx.annotation.VisibleForTesting; import androidx.core.graphics.Insets; import androidx.core.view.WindowInsetsCompat; +import com.android.internal.jank.Cuj; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.BubbleTextView.RunningAppState; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.ActivityAllAppsContainerView; +import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.dot.DotInfo; +import com.android.launcher3.desktop.DesktopAppLaunchTransition; +import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; +import com.android.launcher3.icons.BitmapRenderer; +import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.TaskItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSuspendFlag; import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController; import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.taskbar.bubbles.BubbleBarPinController; +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController; import com.android.launcher3.taskbar.bubbles.BubbleBarView; import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.launcher3.taskbar.bubbles.BubbleControllers; +import com.android.launcher3.taskbar.bubbles.BubbleCreator; import com.android.launcher3.taskbar.bubbles.BubbleDismissController; import com.android.launcher3.taskbar.bubbles.BubbleDragController; import com.android.launcher3.taskbar.bubbles.BubblePinController; -import com.android.launcher3.taskbar.bubbles.BubbleStashController; import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController; +import com.android.launcher3.taskbar.bubbles.DragToBubbleController; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider; +import com.android.launcher3.taskbar.bubbles.stashing.DeviceProfileDimensionsProviderAdapter; +import com.android.launcher3.taskbar.bubbles.stashing.PersistentBubbleStashController; +import com.android.launcher3.taskbar.bubbles.stashing.TransientBubbleStashController; +import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator; +import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator; +import com.android.launcher3.taskbar.growth.NudgeController; import com.android.launcher3.taskbar.navbutton.NearestTouchFrame; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; import com.android.launcher3.taskbar.overlay.TaskbarOverlayController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; @@ -119,29 +155,39 @@ import com.android.launcher3.touch.ItemClickHandler; import com.android.launcher3.touch.ItemClickHandler.ItemClickProxy; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.util.ApiWrapper; +import com.android.launcher3.util.ApplicationInfoWrapper; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.Executors; +import com.android.launcher3.util.LauncherBindableItemsContainer; +import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.NavigationMode; -import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.VibratorWrapper; -import com.android.launcher3.util.ViewCache; import com.android.launcher3.views.ActivityContext; -import com.android.quickstep.LauncherActivityInterface; +import com.android.launcher3.views.BaseDragLayer; import com.android.quickstep.NavHandle; import com.android.quickstep.RecentsModel; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.util.DesktopTask; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SingleTask; +import com.android.quickstep.util.SplitTask; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; +import com.android.systemui.animation.ViewRootSync; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.systemui.unfold.updates.RotationChangeProvider; import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; +import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason; import java.io.PrintWriter; import java.util.Collections; @@ -150,12 +196,9 @@ import java.util.Optional; import java.util.function.Consumer; /** - * The {@link ActivityContext} with which we inflate Taskbar-related Views. This - * allows UI elements - * that are used by both Launcher and Taskbar (such as Folder) to reference a - * generic - * ActivityContext and BaseDragLayer instead of the Launcher activity and its - * DragLayer. + * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements + * that are used by both Launcher and Taskbar (such as Folder) to reference a generic + * ActivityContext and BaseDragLayer instead of the Launcher activity and its DragLayer. */ public class TaskbarActivityContext extends BaseTaskbarContext { @@ -165,6 +208,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { private static final String WINDOW_TITLE = "Taskbar"; + public static final String SIMPLE_VIEW_SETTINGS_KEY = "matcha_enable"; + + protected static final DesktopModeFlag ENABLE_TASKBAR_BEHIND_SHADE = new DesktopModeFlag( + Flags::enableTaskbarBehindShade, false); + private final @Nullable Context mNavigationBarPanelContext; private final TaskbarDragLayer mDragLayer; @@ -173,24 +221,27 @@ public class TaskbarActivityContext extends BaseTaskbarContext { private final WindowManager mWindowManager; private DeviceProfile mDeviceProfile; private WindowManager.LayoutParams mWindowLayoutParams; + private WindowManager.LayoutParams mLastUpdatedLayoutParams; private boolean mIsFullscreen; + private boolean mIsNotificationShadeExpanded = false; // The size we should return to when we call setTaskbarWindowFullscreen(false) private int mLastRequestedNonFullscreenSize; + /** + * When this is true, the taskbar window size is not updated. Requests to update the window + * size are stored in {@link #mLastRequestedNonFullscreenSize} and will take effect after + * bubbles no longer animate and {@link #setTaskbarWindowForAnimatingBubble()} is called. + */ + private boolean mIsTaskbarSizeFrozenForAnimatingBubble; private NavigationMode mNavMode; private boolean mImeDrawsImeNavBar; - private final ViewCache mViewCache = new ViewCache(); private final boolean mIsSafeModeEnabled; - private final boolean mIsUserSetupComplete; - private final boolean mIsNavBarForceVisible; - private final boolean mIsNavBarKidsMode; + private boolean mIsUserSetupComplete; + private boolean mIsNavBarForceVisible; + private boolean mIsNavBarKidsMode; private boolean mIsDestroyed = false; - // The flag to know if the window is excluded from magnification region - // computation. - private boolean mIsExcludeFromMagnificationRegion = false; - private boolean mBindingItems = false; private boolean mAddedWindow = false; // The bounds of the taskbar items relative to TaskbarDragLayer @@ -203,46 +254,54 @@ public class TaskbarActivityContext extends BaseTaskbarContext { private DeviceProfile mPersistentTaskbarDeviceProfile; private final LauncherPrefs mLauncherPrefs; + private final int mPrimaryDisplayId; + private final SystemUiProxy mSysUiProxy; - public TaskbarActivityContext(Context windowContext, + private TaskbarFeatureEvaluator mTaskbarFeatureEvaluator; + + private TaskbarSpecsEvaluator mTaskbarSpecsEvaluator; + + // Snapshot is used to temporarily draw taskbar behind the shade. + private @Nullable View mTaskbarSnapshotView; + private @Nullable TaskbarOverlayContext mTaskbarSnapshotOverlay; + + public TaskbarActivityContext(int displayId, Context windowContext, @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp, TaskbarNavButtonController buttonController, - ScopedUnfoldTransitionProgressProvider unfoldTransitionProgressProvider) { - super(windowContext); - + ScopedUnfoldTransitionProgressProvider unfoldTransitionProgressProvider, + boolean isPrimaryDisplay, int primaryDisplayId, SystemUiProxy sysUiProxy) { + super(windowContext, displayId, isPrimaryDisplay); mNavigationBarPanelContext = navigationBarPanelContext; + mSysUiProxy = sysUiProxy; + mPrimaryDisplayId = primaryDisplayId; applyDeviceProfile(launcherDp); final Resources resources = getResources(); + mTaskbarFeatureEvaluator = TaskbarFeatureEvaluator.getInstance(this); + mTaskbarSpecsEvaluator = new TaskbarSpecsEvaluator( + this, + mTaskbarFeatureEvaluator, + mDeviceProfile.inv.numRows, + mDeviceProfile.inv.numColumns); mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, resources, false); mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode", () -> getPackageManager().isSafeMode()); - // TODO(b/244231596) For shared Taskbar window, update this value in - // applyDeviceProfile() - // instead so to get correct value when recreating the taskbar - SettingsCache settingsCache = SettingsCache.INSTANCE.get(this); - mIsUserSetupComplete = settingsCache.getValue( - Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), 0); - mIsNavBarKidsMode = settingsCache.getValue( - Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), 0); - mIsNavBarForceVisible = mIsNavBarKidsMode; - // Get display and corners first, as views might use them in constructor. - Display display = windowContext.getDisplay(); Context c = getApplicationContext(); mWindowManager = c.getSystemService(WindowManager.class); // Inflate views. - int taskbarLayout = DisplayController.isTransientTaskbar(this) && !isPhoneMode() - ? R.layout.transient_taskbar - : R.layout.taskbar; + boolean isTransientTaskbar = isTransientTaskbar(); + int taskbarLayout = isTransientTaskbar ? R.layout.transient_taskbar : R.layout.taskbar; mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false); TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view); TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim); NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view); StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle); + NudgeView nudgeView = mDragLayer.findViewById(R.id.nudge_icon); BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles); + FrameLayout bubbleBarContainer = mDragLayer.findViewById(R.id.taskbar_bubbles_container); StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle); mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this); @@ -250,18 +309,37 @@ public class TaskbarActivityContext extends BaseTaskbarContext { // If Bubble bar is present, TaskbarControllers depends on it so build it first. Optional bubbleControllersOptional = Optional.empty(); BubbleBarController.onTaskbarRecreated(); - if (BubbleBarController.isBubbleBarEnabled() && bubbleBarView != null) { + final boolean deviceBubbleBarEnabled = enableBubbleBarOnPhones() + || (!mDeviceProfile.getDeviceProperties().isPhone() && !mDeviceProfile.isVerticalBarLayout()); + if (BubbleBarController.isBubbleBarEnabled() && deviceBubbleBarEnabled + && bubbleBarView != null && isPrimaryDisplay) { + Optional bubbleHandleController = Optional.empty(); + Optional bubbleBarSwipeController = Optional.empty(); + if (isTransientTaskbar) { + bubbleHandleController = Optional.of( + new BubbleStashedHandleViewController(this, bubbleHandleView)); + bubbleBarSwipeController = Optional.of(new BubbleBarSwipeController(this)); + } + TaskbarHotseatDimensionsProvider dimensionsProvider = + new DeviceProfileDimensionsProviderAdapter(this); + BubbleStashController bubbleStashController = isTransientTaskbar + ? new TransientBubbleStashController(dimensionsProvider, this) + : new PersistentBubbleStashController(dimensionsProvider); + bubbleStashController.setBubbleBarVerticalCenterForHome( + launcherDp.getBubbleBarVerticalCenterForHome()); bubbleControllersOptional = Optional.of(new BubbleControllers( new BubbleBarController(this, bubbleBarView), - new BubbleBarViewController(this, bubbleBarView), - new BubbleStashController(this), - new BubbleStashedHandleViewController(this, bubbleHandleView), - new BubbleDragController(this), + new BubbleBarViewController(this, bubbleBarView, bubbleBarContainer), + bubbleStashController, + bubbleHandleController, + new BubbleDragController(this, mDragLayer), new BubbleDismissController(this, mDragLayer), - new BubbleBarPinController(this, mDragLayer, - () -> DisplayController.INSTANCE.get(this).getInfo().currentSize), - new BubblePinController(this, mDragLayer, - () -> DisplayController.INSTANCE.get(this).getInfo().currentSize))); + new BubbleBarPinController(this, bubbleBarContainer, this::getScreenSize), + new BubblePinController(this, bubbleBarContainer, this::getScreenSize), + bubbleBarSwipeController, + new DragToBubbleController(this, bubbleBarContainer), + new BubbleCreator(this) + )); } // Construct controllers. @@ -278,7 +356,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers = new TaskbarControllers(this, new TaskbarDragController(this), buttonController, - new NavbarButtonsViewController(this, mNavigationBarPanelContext, navButtonsView), + new NavbarButtonsViewController(this, mNavigationBarPanelContext, navButtonsView, + getMainThreadHandler()), rotationButtonController, new TaskbarDragLayerController(this, mDragLayer), new TaskbarViewController(this, taskbarView), @@ -299,23 +378,29 @@ public class TaskbarActivityContext extends BaseTaskbarContext { new VoiceInteractionWindowController(this), new TaskbarTranslationController(this), new TaskbarSpringOnStashController(this), - new TaskbarRecentAppsController( - RecentsModel.INSTANCE.get(this), - LauncherActivityInterface.INSTANCE::getDesktopVisibilityController), + new TaskbarRecentAppsController(this, RecentsModel.INSTANCE.get(this)), TaskbarEduTooltipController.newInstance(this), new KeyboardQuickSwitchController(), - new TaskbarPinningController(this, - () -> DisplayController.INSTANCE.get(this).getInfo().isInDesktopMode()), - bubbleControllersOptional); + new TaskbarPinningController(this), + bubbleControllersOptional, + new TaskbarDesktopModeController(this, + DesktopVisibilityController.INSTANCE.get(this)), + new NudgeController(this), + new NudgeViewController(this, nudgeView)); mLauncherPrefs = LauncherPrefs.get(this); + onViewCreated(); } - /** Updates {@link DeviceProfile} instances for any Taskbar windows. */ + /** Updates {@link deviceprofile} instances for any Taskbar windows. */ public void updateDeviceProfile(DeviceProfile launcherDp) { applyDeviceProfile(launcherDp); - mControllers.taskbarOverlayController.updateLauncherDeviceProfile(launcherDp); + mControllers.bubbleControllers.ifPresent(bubbleControllers -> { + int bubbleBarVerticalCenter = launcherDp.getBubbleBarVerticalCenterForHome(); + bubbleControllers.bubbleStashController + .setBubbleBarVerticalCenterForHome(bubbleBarVerticalCenter); + }); AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE); // Reapply fullscreen to take potential new screen size into account. setTaskbarWindowFullscreen(mIsFullscreen); @@ -323,9 +408,66 @@ public class TaskbarActivityContext extends BaseTaskbarContext { dispatchDeviceProfileChanged(); } + public final int getPrimaryDisplayId() { + return mPrimaryDisplayId; + } + + @Override + public boolean isTransientTaskbar() { + return DisplayController.isTransientTaskbar(this) && isPrimaryDisplay() && !isPhoneMode(); + } + + @Override + public boolean isPinnedTaskbar() { + return DisplayController.isPinnedTaskbar(this); + } + + @Override + public NavigationMode getNavigationMode() { + return isPrimaryDisplay() ? DisplayController.getNavigationMode(this) + : NavigationMode.THREE_BUTTONS; + } + + @Override + public boolean isInDesktopMode() { + return mControllers != null + && mControllers.taskbarDesktopModeController.isInDesktopMode(getDisplayId()); + } + + @Override + public boolean isTaskbarShowingDesktopTasks() { + return mControllers != null + && mControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar( + getDisplayId()); + } + + @Override + public boolean showLockedTaskbarOnHome() { + return DisplayController.showLockedTaskbarOnHome(this); + } + + @Override + public boolean showDesktopTaskbarForFreeformDisplay() { + return DisplayController.showDesktopTaskbarForFreeformDisplay(this); + } + + @Override + public Point getScreenSize() { + return DisplayController.INSTANCE.get(this).getInfo().currentSize; + } + + @Override + public int getDisplayHeight() { + return DisplayController.INSTANCE.get(this).getInfo().currentSize.y; + } + + @Override + public void notifyConfigChanged() { + DisplayController.INSTANCE.get(this).notifyConfigChange(); + } + /** - * Copy the original DeviceProfile, match the number of hotseat icons and qsb - * width and update + * Copy the original DeviceProfile, match the number of hotseat icons and qsb width and update * the icon size */ private void applyDeviceProfile(DeviceProfile originDeviceProfile) { @@ -336,13 +478,13 @@ public class TaskbarActivityContext extends BaseTaskbarContext { deviceProfile.hotseatQsbWidth = originDeviceProfile.hotseatQsbWidth; // Update icon size - deviceProfile.iconSizePx = deviceProfile.taskbarIconSize; + deviceProfile.iconSizePx = deviceProfile.getTaskbarProfile().getIconSize(); deviceProfile.updateIconSize(1f, this); }; mDeviceProfile = originDeviceProfile.toBuilder(this) .withDimensionsOverride(overrideProvider).build(); - if (DisplayController.isTransientTaskbar(this)) { + if (isTransientTaskbar()) { mTransientTaskbarDeviceProfile = mDeviceProfile; mPersistentTaskbarDeviceProfile = mDeviceProfile .toBuilder(this) @@ -357,24 +499,42 @@ public class TaskbarActivityContext extends BaseTaskbarContext { .setIsTransientTaskbar(true) .build(); } - mNavMode = DisplayController.getNavigationMode(this); + mNavMode = getNavigationMode(); + + SettingsCache settingsCache = SettingsCache.INSTANCE.get(this); + mIsUserSetupComplete = settingsCache.getValue( + Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), 0); + mIsNavBarKidsMode = settingsCache.getValue( + Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), 0); + mIsNavBarForceVisible = mIsNavBarKidsMode; } /** Called when the visibility of the bubble bar changed. */ public void bubbleBarVisibilityChanged(boolean isVisible) { mControllers.uiController.adjustHotseatForBubbleBar(isVisible); - mControllers.taskbarViewController.resetIconAlignmentController(); + mControllers.taskbarViewController.adjustTaskbarForBubbleBar(); } - public void init(@NonNull TaskbarSharedState sharedState) { + /** + * Init of taskbar activity context. + * @param duration If duration is greater than 0, it will be used to create an animation + * for the taskbar create/recreate process. + */ + public void init(@NonNull TaskbarSharedState sharedState, int duration) { mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false); mLastRequestedNonFullscreenSize = getDefaultTaskbarWindowSize(); mWindowLayoutParams = createAllWindowParams(); + mLastUpdatedLayoutParams = new WindowManager.LayoutParams(); + + + AnimatorSet recreateAnim = null; + if (duration > 0) { + recreateAnim = onRecreateAnimation(duration); + } // Initialize controllers after all are constructed. - mControllers.init(sharedState); - // This may not be necessary and can be reverted once we move towards recreating - // all + mControllers.init(sharedState, recreateAnim); + // This may not be necessary and can be reverted once we move towards recreating all // controllers without re-creating the window mControllers.rotationButtonController.onNavigationModeChanged(mNavMode.resValue); updateSysuiStateFlags(sharedState.sysuiStateFlags, true /* fromInit */); @@ -385,12 +545,12 @@ public class TaskbarActivityContext extends BaseTaskbarContext { onNavButtonsDarkIntensityChanged(sharedState.navButtonsDarkIntensity); onNavigationBarLumaSamplingEnabled(sharedState.mLumaSamplingDisplayId, sharedState.mIsLumaSamplingEnabled); + setWallpaperVisible(sharedState.wallpaperVisible); + onTransitionModeUpdated(sharedState.barMode, true /* checkBarModes */); if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { - // W/ the flag not set this entire class gets re-created, which resets the value - // of - // mIsDestroyed. We re-use the class for small-screen, so we explicitly have to - // mark + // W/ the flag not set this entire class gets re-created, which resets the value of + // mIsDestroyed. We re-use the class for small-screen, so we explicitly have to mark // this class as non-destroyed mIsDestroyed = false; } @@ -401,43 +561,95 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } else { notifyUpdateLayoutParams(); } + + + if (recreateAnim != null) { + recreateAnim.start(); + } } /** - * @return {@code true} if the device profile isn't a large screen profile and - * we are using a - * single window for taskbar and navbar. + * Create AnimatorSet for taskbar create/recreate animation. Further used in init + */ + public AnimatorSet onRecreateAnimation(int duration) { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.setDuration(duration); + return animatorSet; + } + + /** + * Called when we want destroy current taskbar with animation as part of recreate process. + */ + public AnimatorSet onDestroyAnimation(int duration) { + mIsDestroyed = true; + AnimatorSet animatorSet = new AnimatorSet(); + mControllers.taskbarViewController.onDestroyAnimation(animatorSet); + mControllers.taskbarDragLayerController.onDestroyAnimation(animatorSet); + animatorSet.setInterpolator(LINEAR); + animatorSet.setDuration(duration); + return animatorSet; + } + + /** + * @return {@code true} if the device profile isn't a large screen profile and we are using a + * single window for taskbar and navbar. */ public boolean isPhoneMode() { return ENABLE_TASKBAR_NAVBAR_UNIFICATION - && mDeviceProfile.isPhone + && mDeviceProfile.getDeviceProperties().isPhone() && !mDeviceProfile.isTaskbarPresent; } + public boolean isTaskbarInMinimalState() { + return mControllers.taskbarViewController.isTaskbarInMinimalState(); + } + /** - * @return {@code true} if {@link #isPhoneMode()} is true and we're using 3 - * button-nav + * @return {@code true} if {@link #isPhoneMode()} is true and we're using 3 button-nav */ public boolean isPhoneButtonNavMode() { return isPhoneMode() && isThreeButtonNav(); } /** - * @return {@code true} if {@link #isPhoneMode()} is true and we're using - * gesture nav + * @return {@code true} if {@link #isPhoneMode()} is true and we're using gesture nav */ public boolean isPhoneGestureNavMode() { return isPhoneMode() && !isThreeButtonNav(); } + /** Returns whether Taskbar draws its own background, vs being translucent for apps to draw. */ + public boolean drawsTaskbarBackground() { + return !isPhoneMode(); + } + /** Returns {@code true} iff a tiny version of taskbar is shown on phone. */ public boolean isTinyTaskbar() { - return enableTinyTaskbar() && mDeviceProfile.isPhone && mDeviceProfile.isTaskbarPresent; + return enableTinyTaskbar() && mDeviceProfile.getDeviceProperties().isPhone() && mDeviceProfile.isTaskbarPresent; + } + + public boolean isBubbleBarOnPhone() { + return enableBubbleBarOnPhones() && enableBubbleBar() && mDeviceProfile.getDeviceProperties().isPhone(); } /** - * Returns if software keyboard is docked or input toolbar is placed at the - * taskbar area + * Returns {@code true} iff bubble bar is enabled (but not necessarily visible / + * containing bubbles). + */ + @Override + public boolean isBubbleBarEnabled() { + return getBubbleControllers() != null && BubbleBarController.isBubbleBarEnabled(); + } + + private boolean isBubbleBarAnimating() { + return mControllers + .bubbleControllers + .map(controllers -> controllers.bubbleBarViewController.isAnimatingNewBubble()) + .orElse(false); + } + + /** + * Returns if software keyboard is docked or input toolbar is placed at the taskbar area */ public boolean isImeDocked() { View dragLayer = getDragLayer(); @@ -446,7 +658,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return false; } - WindowInsetsCompat insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets, dragLayer.getRootView()); + WindowInsetsCompat insetsCompat = + WindowInsetsCompat.toWindowInsetsCompat(insets, dragLayer.getRootView()); if (insetsCompat.isVisible(WindowInsetsCompat.Type.ime())) { Insets imeInsets = insetsCompat.getInsets(WindowInsetsCompat.Type.ime()); @@ -461,17 +674,12 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * Show Taskbar upon receiving broadcast */ public void showTaskbarFromBroadcast() { - mControllers.taskbarStashController.showTaskbarFromBroadcast(); - } - - /** Toggles Taskbar All Apps overlay. */ - public void toggleAllApps() { - mControllers.taskbarAllAppsController.toggle(); - } - - /** Toggles Taskbar All Apps overlay with keyboard ready for search. */ - public void toggleAllAppsSearch() { - mControllers.taskbarAllAppsController.toggleSearch(); + // If user is in middle of taskbar education handle go to next step of education + if (mControllers.taskbarEduTooltipController.isBeforeTooltipFeaturesStep()) { + mControllers.taskbarEduTooltipController.hide(); + mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); + } + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false); } @Override @@ -482,6 +690,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { @Override public void dispatchDeviceProfileChanged() { super.dispatchDeviceProfileChanged(); + Trace.instantForTrack(TRACE_TAG_APP, "TaskbarActivityContext#DeviceProfileChanged", + getDeviceProfile().toSmallString()); } @NonNull @@ -507,19 +717,16 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } /** - * Creates LayoutParams for adding a view directly to WindowManager as a new - * window. + * Creates LayoutParams for adding a view directly to WindowManager as a new window. * - * @param type The window type to pass to the created - * WindowManager.LayoutParams. - * @param title The window title to pass to the created - * WindowManager.LayoutParams. + * @param type The window type to pass to the created WindowManager.LayoutParams. + * @param title The window title to pass to the created WindowManager.LayoutParams. */ public WindowManager.LayoutParams createDefaultWindowLayoutParams(int type, String title) { int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_SLIPPERY - | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; - if (DisplayController.isTransientTaskbar(this) && !isRunningInTestHarness()) { + | WindowManager.LayoutParams.FLAG_SLIPPERY; + boolean watchOutside = isTransientTaskbar() || isThreeButtonNav(); + if (watchOutside && !isRunningInTestHarness()) { windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; } @@ -536,7 +743,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { windowLayoutParams.receiveInsetsIgnoringZOrder = true; windowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; windowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - windowLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + windowLayoutParams.privateFlags = + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; windowLayoutParams.accessibilityTitle = getString( isPhoneMode() ? R.string.taskbar_phone_a11y_title : R.string.taskbar_a11y_title); @@ -544,19 +752,21 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } /** - * Creates {@link WindowManager.LayoutParams} for Taskbar, and also sets - * LP.paramsForRotation + * Creates {@link WindowManager.LayoutParams} for Taskbar, and also sets LP.paramsForRotation * for taskbar */ private WindowManager.LayoutParams createAllWindowParams() { - final int windowType = ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL; - WindowManager.LayoutParams windowLayoutParams = createDefaultWindowLayoutParams(windowType, - TaskbarActivityContext.WINDOW_TITLE); + final int windowType = + (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isPrimaryDisplay()) ? TYPE_NAVIGATION_BAR + : TYPE_NAVIGATION_BAR_PANEL; + WindowManager.LayoutParams windowLayoutParams = + createDefaultWindowLayoutParams(windowType, TaskbarActivityContext.WINDOW_TITLE); windowLayoutParams.paramsForRotation = new WindowManager.LayoutParams[4]; for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { - WindowManager.LayoutParams lp = createDefaultWindowLayoutParams(windowType, - TaskbarActivityContext.WINDOW_TITLE); + WindowManager.LayoutParams lp = + createDefaultWindowLayoutParams(windowType, + TaskbarActivityContext.WINDOW_TITLE); if (isPhoneButtonNavMode()) { populatePhoneButtonNavModeWindowLayoutParams(rot, lp); } @@ -564,7 +774,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } // Override with current layout params - WindowManager.LayoutParams currentParams = windowLayoutParams.paramsForRotation[getDisplay().getRotation()]; + WindowManager.LayoutParams currentParams = + windowLayoutParams.paramsForRotation[getDisplay().getRotation()]; windowLayoutParams.width = currentParams.width; windowLayoutParams.height = currentParams.height; windowLayoutParams.gravity = currentParams.gravity; @@ -573,8 +784,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } /** - * Update {@link WindowManager.LayoutParams} with values specific to phone and 3 - * button + * Update {@link WindowManager.LayoutParams} with values specific to phone and 3 button * navigation users */ private void populatePhoneButtonNavModeWindowLayoutParams(int rot, @@ -610,6 +820,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mNavMode == NavigationMode.THREE_BUTTONS; } + /** Returns whether taskbar should start align. */ + public boolean shouldStartAlignTaskbar() { + return isThreeButtonNav() && mDeviceProfile.getTaskbarProfile().isStartAlignTaskbar(); + } + public boolean isGestureNav() { return mNavMode == NavigationMode.NO_BUTTON; } @@ -619,9 +834,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } public int getCornerRadius() { - return isPhoneMode() ? 0 - : getResources().getDimensionPixelSize( - R.dimen.persistent_taskbar_corner_radius); + return isPhoneMode() ? 0 : getResources().getDimensionPixelSize( + R.dimen.persistent_taskbar_corner_radius); } public WindowManager.LayoutParams getWindowLayoutParams() { @@ -643,6 +857,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mControllers.taskbarDragController; } + @Override + public ModelWriter getModelWriter() { + return mControllers.taskbarViewController.getModelWriter(); + } + @Nullable public BubbleControllers getBubbleControllers() { return mControllers.bubbleControllers.orElse(null); @@ -653,11 +872,6 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mControllers.stashedHandleViewController; } - @Override - public ViewCache getViewCache() { - return mViewCache; - } - @Override public View.OnClickListener getItemOnClickListener() { return this::onTaskbarIconClicked; @@ -673,14 +887,16 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } LauncherAtom.ContainerInfo oldContainer = itemInfoBuilder.getContainerInfo(); - LauncherAtom.TaskBarContainer.Builder taskbarBuilder = LauncherAtom.TaskBarContainer.newBuilder(); + LauncherAtom.TaskBarContainer.Builder taskbarBuilder = + LauncherAtom.TaskBarContainer.newBuilder(); if (mControllers.uiController.isInOverviewUi()) { taskbarBuilder.setTaskSwitcherContainer( LauncherAtom.TaskSwitcherContainer.newBuilder()); } if (oldContainer.hasPredictedHotseatContainer()) { - LauncherAtom.PredictedHotseatContainer predictedHotseat = oldContainer.getPredictedHotseatContainer(); + LauncherAtom.PredictedHotseatContainer predictedHotseat = + oldContainer.getPredictedHotseatContainer(); if (predictedHotseat.hasIndex()) { taskbarBuilder.setIndex(predictedHotseat.getIndex()); @@ -724,31 +940,28 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } } - @Override - public DotInfo getDotInfoForItem(ItemInfo info) { - return getPopupDataProvider().getDotInfoForItem(info); - } - @NonNull @Override public PopupDataProvider getPopupDataProvider() { return mControllers.taskbarPopupController.getPopupDataProvider(); } + @NonNull + @Override + public LauncherBindableItemsContainer getContent() { + return mControllers.taskbarViewController.getContent(); + } + + @Override + public ActivityAllAppsContainerView getAppsView() { + return mControllers.taskbarAllAppsController.getAppsView(); + } + @Override public View.AccessibilityDelegate getAccessibilityDelegate() { return mAccessibilityDelegate; } - @Override - public boolean isBindingItems() { - return mBindingItems; - } - - public void setBindingItems(boolean bindingItems) { - mBindingItems = bindingItems; - } - @Override public void onDragStart() { setTaskbarWindowFullscreen(true); @@ -761,7 +974,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { @Override public void onPopupVisibilityChanged(boolean isVisible) { - setTaskbarWindowFocusable(isVisible); + setTaskbarWindowFocusable(isVisible /* focusable */, false /* imeFocusable */); } @Override @@ -778,11 +991,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext { public ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) { RunnableList callbacks = new RunnableList(); ActivityOptions options = ActivityOptions.makeCustomAnimation(this, 0, 0); - if (Utilities.ATLEAST_T) { - options.setSplashScreenStyle(splashScreenStyle); - } - Utilities.allowBGLaunch(options); - IRemoteCallback endCallback = completeRunnableListCallback(callbacks); + options.setSplashScreenStyle(splashScreenStyle); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + IRemoteCallback endCallback = completeRunnableListCallback(callbacks, this); options.setOnAnimationAbortListener(endCallback); options.setOnAnimationFinishedListener(endCallback); @@ -794,11 +1006,23 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED); } + private ActivityOptionsWrapper getActivityLaunchDesktopOptions() { + ActivityOptions options = ActivityOptions.makeRemoteTransition( + createDesktopAppLaunchRemoteTransition( + AppLaunchType.LAUNCH, Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)); + return new ActivityOptionsWrapper(options, new RunnableList()); + } + /** * Sets a new data-source for this taskbar instance */ public void setUIController(@NonNull TaskbarUIController uiController) { mControllers.setUiController(uiController); + if (BubbleBarController.isBubbleBarEnabled() && mControllers.bubbleControllers.isEmpty()) { + // if the bubble bar was visible in a previous configuration of taskbar and is being + // recreated now without bubbles, clean up any bubble bar adjustments from hotseat + bubbleBarVisibilityChanged(/* isVisible= */ false); + } } /** @@ -808,11 +1032,39 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.taskbarStashController.setSetupUIVisible(isVisible); } + public void setWallpaperVisible(boolean isVisible) { + mControllers.navbarButtonsViewController.setWallpaperVisible(isVisible); + } + + public void checkNavBarModes() { + mControllers.navbarButtonsViewController.checkNavBarModes(); + } + + public void finishBarAnimations() { + mControllers.navbarButtonsViewController.finishBarAnimations(); + } + + public void touchAutoDim(boolean reset) { + mControllers.navbarButtonsViewController.touchAutoDim(reset); + } + + public void transitionTo(@BarTransitions.TransitionMode int barMode, + boolean animate) { + mControllers.navbarButtonsViewController.transitionTo(barMode, animate); + } + + public void appTransitionPending(boolean pending) { + mControllers.stashedHandleViewController.setIsAppTransitionPending(pending); + } + /** * Called when this instance of taskbar is no longer needed */ public void onDestroy() { + onViewDestroyed(); + removeTaskbarSnapshot(); mIsDestroyed = true; + mTaskbarFeatureEvaluator.onDestroy(); setUIController(TaskbarUIController.DEFAULT); mControllers.onDestroy(); if (!enableTaskbarNoRecreate() && !ENABLE_TASKBAR_NAVBAR_UNIFICATION) { @@ -830,7 +1082,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.navbarButtonsViewController.updateStateForSysuiFlags(systemUiStateFlags, fromInit); boolean isShadeVisible = (systemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0; - onNotificationShadeExpandChanged(isShadeVisible, fromInit); + onNotificationShadeExpandChanged(isShadeVisible, fromInit || isPhoneMode()); mControllers.taskbarViewController.setRecentsButtonDisabled( mControllers.navbarButtonsViewController.isRecentsDisabled() || isNavBarKidsModeActive()); @@ -849,25 +1101,124 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.uiController.updateStateForSysuiFlags(systemUiStateFlags); mControllers.bubbleControllers.ifPresent(controllers -> { controllers.bubbleBarController.updateStateForSysuiFlags(systemUiStateFlags); - controllers.bubbleStashedHandleViewController.setIsHomeButtonDisabled( - mControllers.navbarButtonsViewController.isHomeDisabled()); + controllers.bubbleStashedHandleViewController.ifPresent(controller -> + controller.setIsHomeButtonDisabled( + mControllers.navbarButtonsViewController.isHomeDisabled())); }); } /** - * Hides the taskbar icons and background when the notication shade is expanded. + * Hides the taskbar icons and background when the notification shade is expanded. */ private void onNotificationShadeExpandChanged(boolean isExpanded, boolean skipAnim) { + boolean isExpandedUpdated = isExpanded != mIsNotificationShadeExpanded; + mIsNotificationShadeExpanded = isExpanded; + // Close all floating views within the Taskbar window to make sure nothing is shown over + // the notification shade. + if (isExpanded) { + AbstractFloatingView.closeAllOpenViewsExcept(this, TYPE_TASKBAR_OVERLAY_PROXY); + } + float alpha = isExpanded ? 0 : 1; AnimatorSet anim = new AnimatorSet(); anim.play(mControllers.taskbarViewController.getTaskbarIconAlpha().get( TaskbarViewController.ALPHA_INDEX_NOTIFICATION_EXPANDED).animateToValue(alpha)); anim.play(mControllers.taskbarDragLayerController.getNotificationShadeBgTaskbar() .animateToValue(alpha)); + + if (isExpandedUpdated) { + mControllers.bubbleControllers.ifPresent(controllers -> { + BubbleBarViewController bubbleBarViewController = + controllers.bubbleBarViewController; + anim.play(bubbleBarViewController.getBubbleBarAlpha().get(0).animateToValue(alpha)); + MultiPropertyFactory.MultiProperty handleAlpha = + controllers.bubbleStashController.getHandleViewAlpha(); + if (handleAlpha != null) { + anim.play(handleAlpha.animateToValue(alpha)); + } + }); + } anim.start(); if (skipAnim) { anim.end(); } + + updateTaskbarSnapshot(anim, isExpanded); + } + + private void updateTaskbarSnapshot(AnimatorSet anim, boolean isExpanded) { + if (!ENABLE_TASKBAR_BEHIND_SHADE.isTrue() + || isPhoneMode()) { + return; + } + if (mTaskbarSnapshotView == null) { + mTaskbarSnapshotView = new View(this); + } + if (isExpanded) { + if (!mTaskbarSnapshotView.isAttachedToWindow() + && mDragLayer.isAttachedToWindow() + && mDragLayer.isLaidOut() + && mDragLayer.isVisibleToUser() + && mTaskbarSnapshotView.getParent() == null) { + NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view); + int oldNavButtonsVisibility = navButtonsView.getVisibility(); + navButtonsView.setVisibility(View.INVISIBLE); + + Drawable drawable = new FastBitmapDrawable(BitmapRenderer.createHardwareBitmap( + mDragLayer.getWidth(), + mDragLayer.getHeight(), + mDragLayer::draw)); + + navButtonsView.setVisibility(oldNavButtonsVisibility); + mTaskbarSnapshotView.setBackground(drawable); + mTaskbarSnapshotView.setAlpha(0f); + + mTaskbarSnapshotView.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) { + mTaskbarSnapshotView.removeOnAttachStateChangeListener(this); + anim.end(); + mTaskbarSnapshotView.setAlpha(1f); + if (!Utilities.isRunningInTestHarness()) { + ViewRootSync.synchronizeNextDraw(mDragLayer, + mTaskbarSnapshotView, + () -> {}); + } + } + + @Override + public void onViewDetachedFromWindow(@NonNull View v) {} + }); + BaseDragLayer.LayoutParams layoutParams = new BaseDragLayer.LayoutParams( + mDragLayer.getWidth(), mDragLayer.getHeight()); + layoutParams.gravity = mWindowLayoutParams.gravity; + layoutParams.ignoreInsets = true; + mTaskbarSnapshotOverlay = mControllers.taskbarOverlayController.requestWindow(); + mTaskbarSnapshotOverlay.getDragLayer().addView(mTaskbarSnapshotView, layoutParams); + } + } else { + if (mTaskbarSnapshotView.isAttachedToWindow()) { + mTaskbarSnapshotView.setAlpha(0f); + anim.end(); + if (Utilities.isRunningInTestHarness()) { + removeTaskbarSnapshot(); + } else { + ViewRootSync.synchronizeNextDraw(mDragLayer, mTaskbarSnapshotView, + this::removeTaskbarSnapshot); + } + } else { + removeTaskbarSnapshot(); + } + } + } + + private void removeTaskbarSnapshot() { + if (mTaskbarSnapshotOverlay != null) { + mTaskbarSnapshotOverlay.getDragLayer().removeView(mTaskbarSnapshotView); + } + mTaskbarSnapshotView = null; + mTaskbarSnapshotOverlay = null; } public void onRotationProposal(int rotation, boolean isValid) { @@ -885,9 +1236,13 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.rotationButtonController.onBehaviorChanged(displayId, behavior); } + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + mControllers.navbarButtonsViewController.onTransitionModeUpdated(barMode, checkBarModes); + } + public void onNavButtonsDarkIntensityChanged(float darkIntensity) { - mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity() - .updateValue(darkIntensity); + mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity().updateValue( + darkIntensity); } public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { @@ -911,19 +1266,40 @@ public class TaskbarActivityContext extends BaseTaskbarContext { setTaskbarWindowSize(fullscreen ? MATCH_PARENT : mLastRequestedNonFullscreenSize); } + /** + * Updates the taskbar window size according to whether bubbles are animating. + * + *

This method should be called when bubbles start animating and again after the animation is + * complete. + */ + public void setTaskbarWindowForAnimatingBubble() { + if (isBubbleBarAnimating()) { + // the default window size accounts for the bubble flyout + setTaskbarWindowSize(getDefaultTaskbarWindowSize()); + mIsTaskbarSizeFrozenForAnimatingBubble = true; + } else { + mIsTaskbarSizeFrozenForAnimatingBubble = false; + setTaskbarWindowSize( + mLastRequestedNonFullscreenSize != 0 + ? mLastRequestedNonFullscreenSize : getDefaultTaskbarWindowSize()); + } + } + /** * Called when drag ends or when a view is removed from the DragLayer. */ void onDragEndOrViewRemoved() { boolean isDragInProgress = mControllers.taskbarDragController.isSystemDragInProgress(); - // Overlay AFVs are in a separate window and do not require Taskbar to be - // fullscreen. + // Overlay AFVs are in a separate window and do not require Taskbar to be fullscreen. if (!isDragInProgress && !AbstractFloatingView.hasOpenView( - this, TYPE_ALL & ~TYPE_TASKBAR_OVERLAY_PROXY)) { + this, TYPE_ALL & ~TYPE_TASKBAR_OVERLAY_PROXY)) { // Reverts Taskbar window to its original size - setTaskbarWindowFullscreen(false); + Runnable resetTaskbarFullscreen = () -> setTaskbarWindowFullscreen(false); + mControllers.bubbleControllers.ifPresentOrElse( + bc -> bc.dragToBubbleController.runAfterDropTargetsHidden( + resetTaskbarFullscreen), resetTaskbarFullscreen); } setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, isDragInProgress); @@ -934,31 +1310,27 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } /** - * Updates the TaskbarContainer size (pass - * {@link #getDefaultTaskbarWindowSize()} to reset). + * Updates the TaskbarContainer size (pass {@link #getDefaultTaskbarWindowSize()} to reset). */ public void setTaskbarWindowSize(int size) { - // In landscape phone button nav mode, we should set the task bar width instead - // of height - // because this is the only case in which the nav bar is not on the display - // bottom. - boolean landscapePhoneButtonNav = isPhoneButtonNavMode() && mDeviceProfile.isLandscape; - if ((landscapePhoneButtonNav ? mWindowLayoutParams.width : mWindowLayoutParams.height) == size - || mIsDestroyed) { + // In landscape phone button nav mode, we should set the task bar width instead of height + // because this is the only case in which the nav bar is not on the display bottom. + boolean landscapePhoneButtonNav = isPhoneButtonNavMode() && mDeviceProfile.getDeviceProperties().isLandscape(); + if ((landscapePhoneButtonNav ? mWindowLayoutParams.width : mWindowLayoutParams.height) + == size || mIsDestroyed) { return; } if (size == MATCH_PARENT) { - size = mDeviceProfile.heightPx; + size = mDeviceProfile.getDeviceProperties().getHeightPx(); } else { mLastRequestedNonFullscreenSize = size; - if (mIsFullscreen) { - // We still need to be fullscreen, so defer any change to our height until we - // call - // setTaskbarWindowFullscreen(false). For example, this could happen when - // dragging - // from the gesture region, as the drag will cancel the gesture and reset - // launcher's - // state, which in turn normally would reset the taskbar window height as well. + if (mIsFullscreen || mIsTaskbarSizeFrozenForAnimatingBubble) { + // We either still need to be fullscreen or a bubble is still animating, so defer + // any change to our height until setTaskbarWindowFullscreen(false) is called or + // setTaskbarWindowForAnimatingBubble() is called after the bubble animation + // completed. For example, this could happen when dragging from the gesture region, + // as the drag will cancel the gesture and reset launcher's state, which in turn + // normally would reset the taskbar window height as well. return; } } @@ -974,53 +1346,61 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } } mControllers.runAfterInit( - mControllers.taskbarInsetsController::onTaskbarOrBubblebarWindowHeightOrInsetsChanged); + mControllers.taskbarInsetsController + ::onTaskbarOrBubblebarWindowHeightOrInsetsChanged); notifyUpdateLayoutParams(); } /** - * Returns the default size (in most cases height, but in 3-button phone mode, - * width) of the + * Returns the default size (in most cases height, but in 3-button phone mode, width) of the * window, including the static corner radii above taskbar. */ public int getDefaultTaskbarWindowSize() { Resources resources = getResources(); if (isPhoneMode()) { - return isThreeButtonNav() ? resources.getDimensionPixelSize(R.dimen.taskbar_phone_size) - : resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size); + return isThreeButtonNav() ? + resources.getDimensionPixelSize(R.dimen.taskbar_phone_size) : + resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size); } if (!isUserSetupComplete()) { return getSetupWindowSize(); } - boolean shouldTreatAsTransient = DisplayController.isTransientTaskbar(this) - || (enableTaskbarPinning() && !isThreeButtonNav()); + int bubbleBarTop = mControllers.bubbleControllers.map(bubbleControllers -> + bubbleControllers.bubbleBarViewController.getBubbleBarWithFlyoutMaximumHeight() + ).orElse(0); + int taskbarWindowSize; + boolean shouldTreatAsTransient = + isTransientTaskbar() || (enableTaskbarPinning() + && mTaskbarFeatureEvaluator.getSupportsTransitionToTransientTaskbar()); int extraHeightForTaskbarTooltips = enableCursorHoverStates() ? resources.getDimensionPixelSize(R.dimen.arrow_toast_arrow_height) - + (resources.getDimensionPixelSize(R.dimen.taskbar_tooltip_vertical_padding) * 2) - + calculateTextHeight( - resources.getDimensionPixelSize(R.dimen.arrow_toast_text_size)) + + (resources.getDimensionPixelSize(R.dimen.taskbar_tooltip_vertical_padding) * 2) + + calculateTextHeight( + resources.getDimensionPixelSize(R.dimen.arrow_toast_text_size)) : 0; - // Return transient taskbar window height when pinning feature is enabled, so - // taskbar view + // Return transient taskbar window height when pinning feature is enabled, so taskbar view // does not get cut off during pinning animation. if (shouldTreatAsTransient) { DeviceProfile transientTaskbarDp = mDeviceProfile.toBuilder(this) .setIsTransientTaskbar(true).build(); - return transientTaskbarDp.taskbarHeight - + (2 * transientTaskbarDp.taskbarBottomMargin) + taskbarWindowSize = transientTaskbarDp.getTaskbarProfile().getHeight() + + (2 * transientTaskbarDp.getTaskbarProfile().getBottomMargin()) + Math.max(extraHeightForTaskbarTooltips, resources.getDimensionPixelSize( - R.dimen.transient_taskbar_shadow_blur)); + R.dimen.transient_taskbar_shadow_blur)); + return Math.max(taskbarWindowSize, bubbleBarTop); } - return mDeviceProfile.taskbarHeight + + taskbarWindowSize = mDeviceProfile.getTaskbarProfile().getHeight() + getCornerRadius() + extraHeightForTaskbarTooltips; + return Math.max(taskbarWindowSize, bubbleBarTop); } public int getSetupWindowSize() { @@ -1036,35 +1416,55 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } /** - * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} - * on the taskbar - * window. + * Sets whether the taskbar window should be focusable and IME focusable. This won't be IME + * focusable unless it is also focusable. + * + * @param focusable whether it should be focusable. + * @param imeFocusable whether it should be IME focusable. + * + * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE + * @see WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM */ - public void setTaskbarWindowFocusable(boolean focusable) { - if (focusable) { - mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; - } - notifyUpdateLayoutParams(); - } - - /** - * Applies forcibly show flag to taskbar window iff transient taskbar is - * unstashed. - */ - public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) { - if (!DisplayController.isTransientTaskbar(this)) { + public void setTaskbarWindowFocusable(boolean focusable, boolean imeFocusable) { + if (isPhoneMode()) { return; } + if (focusable) { + mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; + if (imeFocusable) { + mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM; + } else { + mWindowLayoutParams.flags |= FLAG_ALT_FOCUSABLE_IM; + } + } else { + mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; + mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM; + } notifyUpdateLayoutParams(); } /** - * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} - * on the taskbar - * window. If we're now focusable, also move nav buttons to a separate window - * above IME. + * Applies forcibly show flag to taskbar window iff transient taskbar is unstashed. + */ + public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) { + if (!isTransientTaskbar() || isPhoneMode()) { + return; + } + if (shouldForceShow) { + mWindowLayoutParams.forciblyShownTypes |= WindowInsets.Type.navigationBars(); + } else { + mWindowLayoutParams.forciblyShownTypes &= ~WindowInsets.Type.navigationBars(); + } + notifyUpdateLayoutParams(); + } + + /** + * Sets whether the taskbar window should be focusable, as well as IME focusable. If we're now + * focusable, also move nav buttons to a separate window above IME. + * + * @param focusable whether it should be focusable. + * + * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE */ public void setTaskbarWindowFocusableForIme(boolean focusable) { if (focusable) { @@ -1072,13 +1472,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } else { mControllers.navbarButtonsViewController.moveNavButtonsBackToTaskbarWindow(); } - setTaskbarWindowFocusable(focusable); + setTaskbarWindowFocusable(focusable, true /* imeFocusable */); } - /** - * Adds the given view to WindowManager with the provided LayoutParams (creates - * new window). - */ + /** Adds the given view to WindowManager with the provided LayoutParams (creates new window). */ public void addWindowView(View view, WindowManager.LayoutParams windowLayoutParams) { if (!view.isAttachedToWindow()) { mWindowManager.addView(view, windowLayoutParams); @@ -1097,15 +1494,50 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.uiController.startSplitSelection(splitSelectSource); } + // If in overview, and a desktop task is available, launches the overview desktop task and + // schedules the provided runnable. + // Returns whether the runnable has been posted. + private boolean runAfterLaunchingDesktopTaskIfInOverview( + RecentsView recents, + Runnable runnableToRun) { + if (recents == null || !isTaskbarShowingDesktopTasks() + || !mControllers.uiController.isInOverviewUi()) { + return false; + } + + RunnableList runnableList = recents.launchRunningDesktopTaskView(); + // Wrapping it in runnable so we post after DW is ready for the app + // launch. + if (runnableList == null) { + return false; + } + + runnableList.add(() -> UI_HELPER_EXECUTOR.execute(runnableToRun)); + return true; + } + protected void onTaskbarIconClicked(View view) { TaskbarUIController taskbarUIController = mControllers.uiController; RecentsView recents = taskbarUIController.getRecentsView(); boolean shouldCloseAllOpenViews = true; Object tag = view.getTag(); - if (tag instanceof Task) { - Task task = (Task) tag; - ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key, - ActivityOptions.makeBasic()); + + mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false); + + // TODO: b/316004172, b/343289567: Handle `DesktopTask` and `SplitTask`. + if (tag instanceof SingleTask singleTask) { + RemoteTransition remoteTransition = + (isTaskbarShowingDesktopTasks() && canUnminimizeDesktopTask( + singleTask.getTask().key.id)) + ? createDesktopAppLaunchRemoteTransition(AppLaunchType.UNMINIMIZE, + Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON) + : null; + Runnable launchTask = () -> handleGroupTaskLaunch(singleTask, remoteTransition, + isTaskbarShowingDesktopTasks(), DesktopTaskToFrontReason.TASKBAR_TAP); + if (!runAfterLaunchingDesktopTaskIfInOverview(recents, launchTask)) { + launchTask.run(); + } + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } else if (tag instanceof FolderInfo) { // Tapping an expandable folder icon on Taskbar @@ -1122,6 +1554,22 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.uiController.onTaskbarIconLaunched(api); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } + } else if (tag instanceof TaskItemInfo info) { + RemoteTransition remoteTransition = canUnminimizeDesktopTask(info.getTaskId()) + ? createDesktopAppLaunchRemoteTransition( + AppLaunchType.UNMINIMIZE, Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON) + : null; + + Runnable launchTask = () -> + SystemUiProxy.INSTANCE.get(this).showDesktopApp( + info.getTaskId(), remoteTransition, + DesktopTaskToFrontReason.TASKBAR_TAP); + if (!runAfterLaunchingDesktopTaskIfInOverview(recents, launchTask)) { + UI_HELPER_EXECUTOR.execute(launchTask); + } + + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar( + /* stash= */ true); } else if (tag instanceof WorkspaceItemInfo) { // Tapping a launchable icon on Taskbar WorkspaceItemInfo info = (WorkspaceItemInfo) tag; @@ -1134,7 +1582,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { Intent intent = new Intent(info.getIntent()) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { - if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) { + if (mIsSafeModeEnabled + && !new ApplicationInfoWrapper(this, intent).isSystem()) { Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); } else if (info.isPromise()) { @@ -1156,8 +1605,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } } catch (NullPointerException - | ActivityNotFoundException - | SecurityException e) { + | ActivityNotFoundException + | SecurityException e) { Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT) .show(); Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e); @@ -1185,8 +1634,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } else if (tag instanceof AppInfo) { // Tapping an item in AllApps AppInfo info = (AppInfo) tag; - if (recents != null - && taskbarUIController.getRecentsView().isSplitSelectionActive()) { + if (recents != null && recents.isSplitSelectionActive()) { // If we are selecting a second app for split, launch the split tasks taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); } else { @@ -1200,14 +1648,80 @@ public class TaskbarActivityContext extends BaseTaskbarContext { Log.e(TAG, "Unknown type clicked: " + tag); } + mControllers.taskbarPopupController.maybeCloseMultiInstanceMenu(); if (shouldCloseAllOpenViews) { AbstractFloatingView.closeAllOpenViews(this); } } /** - * Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or - * inside an app), + * Launches the given GroupTask with the following behavior: + * - If the GroupTask is a DesktopTask, launch the tasks in that Desktop. + * - If {@code onDesktop}, bring the given GroupTask to the front. + * - If the GroupTask is a single task, launch it via startActivityFromRecents. + * - Otherwise, we assume the GroupTask is a Split pair and launch them together. + *

+ * Given start and/or finish callbacks, they will be run before an after the app launch + * respectively in cases where we can't use the remote transition, otherwise we will assume that + * these callbacks are included in the remote transition. + */ + public void handleGroupTaskLaunch( + GroupTask task, + @Nullable RemoteTransition remoteTransition, + boolean onDesktop, + DesktopTaskToFrontReason toFrontReason) { + if (task instanceof DesktopTask) { + UI_HELPER_EXECUTOR.execute( + () -> SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplayId(), + remoteTransition)); + return; + } + if (onDesktop && task instanceof SingleTask singleTask) { + boolean useRemoteTransition = canUnminimizeDesktopTask(singleTask.getTask().key.id); + UI_HELPER_EXECUTOR.execute(() -> { + SystemUiProxy.INSTANCE.get(this).showDesktopApp(singleTask.getTask().key.id, + useRemoteTransition ? remoteTransition : null, toFrontReason); + }); + return; + } + if (task instanceof SingleTask singleTask) { + UI_HELPER_EXECUTOR.execute(() -> { + ActivityOptions activityOptions = + makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options; + activityOptions.setRemoteTransition(remoteTransition); + + ActivityManagerWrapper.getInstance().startActivityFromRecents( + singleTask.getTask().key, activityOptions); + }); + return; + } + assert task instanceof SplitTask; + mControllers.uiController.launchSplitTasks((SplitTask) task, remoteTransition); + } + + /** Returns whether the given task is minimized and can be unminimized. */ + public boolean canUnminimizeDesktopTask(int taskId) { + BubbleTextView.RunningAppState runningAppState = + mControllers.taskbarRecentAppsController.getRunningAppState(taskId); + Log.d(TAG, "Task id=" + taskId + ", Running app state=" + runningAppState); + return runningAppState == RunningAppState.MINIMIZED + && DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX.isTrue(); + } + + private RemoteTransition createDesktopAppLaunchRemoteTransition( + AppLaunchType appLaunchType, @Cuj.CujType int cujType) { + return new RemoteTransition( + new DesktopAppLaunchTransition( + this, + appLaunchType, + cujType, + getMainExecutor() + ), + "TaskbarDesktopAppLaunch"); + } + + /** + * Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app), * and calls the appropriate method to animate and launch. */ private void launchFromTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, @@ -1224,7 +1738,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext { */ private void launchFromInAppTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, List itemInfos) { - if (recents == null) { + boolean launchedFromExternalDisplay = + DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() + && !isPrimaryDisplay(); + if (recents == null && !launchedFromExternalDisplay) { return; } @@ -1232,24 +1749,20 @@ public class TaskbarActivityContext extends BaseTaskbarContext { if (tappedAppPair) { // If the icon is an app pair, the logic gets a bit complicated because we play - // different animations depending on which app (or app pair) is currently - // running on + // different animations depending on which app (or app pair) is currently running on // screen, so delegate logic to appPairsController. recents.getSplitSelectController().getAppPairsController() .handleAppPairLaunchInApp((AppPairIcon) launchingIconView, itemInfos); } else { // Tapped a single app, nothing complicated here. - startItemInfoActivity(itemInfos.get(0), null /* foundTask */); + startItemInfoActivity(itemInfos.get(0), null /*foundTask*/); } } /** - * Run when the user taps a Taskbar icon while in Overview. If the tapped app is - * currently - * visible to the user in Overview, or is part of a visible split pair, we - * expand the TaskView - * as if the user tapped on it (preserving the split pair). Otherwise, launch it - * normally + * Run when the user taps a Taskbar icon while in Overview. If the tapped app is currently + * visible to the user in Overview, or is part of a visible split pair, we expand the TaskView + * as if the user tapped on it (preserving the split pair). Otherwise, launch it normally * (potentially breaking a split pair). */ private void launchFromOverviewTaskbar(@Nullable RecentsView recents, @@ -1260,20 +1773,21 @@ public class TaskbarActivityContext extends BaseTaskbarContext { boolean isLaunchingAppPair = itemInfos.size() == 2; // Convert the list of ItemInfo instances to a list of ComponentKeys - List componentKeys = itemInfos.stream().map(ItemInfo::getComponentKey).collect(toList()); + List componentKeys = + itemInfos.stream().map(ItemInfo::getComponentKey).toList(); recents.getSplitSelectController().findLastActiveTasksAndRunCallback( componentKeys, isLaunchingAppPair, foundTasks -> { - @Nullable - Task foundTask = foundTasks[0]; + @Nullable Task foundTask = foundTasks[0]; if (foundTask != null) { TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id); if (foundTaskView != null - && foundTaskView.isVisibleToUser()) { + && foundTaskView.isVisibleToUser() + && !(foundTaskView instanceof DesktopTaskView)) { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon"); - foundTaskView.launchTasks(); + foundTaskView.launchWithAnimation(); return; } } @@ -1282,24 +1796,28 @@ public class TaskbarActivityContext extends BaseTaskbarContext { // Finish recents animation if it's running before launching to ensure // we get both leashes for the animation mControllers.uiController.setSkipNextRecentsAnimEnd(); - recents.switchToScreenshot(() -> recents.finishRecentsAnimation(true /* toRecents */, - false /* shouldPip */, - () -> recents - .getSplitSelectController() - .getAppPairsController() - .launchAppPair((AppPairIcon) launchingIconView, - -1 /* cuj */))); + recents.switchToScreenshot(() -> + recents.finishRecentsAnimation(true /*toRecents*/, + false /*shouldPip*/, + () -> recents + .getSplitSelectController() + .getAppPairsController() + .launchAppPair((AppPairIcon) launchingIconView, + -1 /*cuj*/))); } else { - startItemInfoActivity(itemInfos.get(0), foundTask); + Runnable launchTask = + () -> startItemInfoActivity(itemInfos.get(0), foundTask); + if (!runAfterLaunchingDesktopTaskIfInOverview(recents, launchTask)) { + launchTask.run(); + } } - }); + } + ); } /** - * Starts an activity with the information provided by the "info" param. - * However, if - * taskInRecents is present, it will prioritize re-launching an existing - * instance via + * Starts an activity with the information provided by the "info" param. However, if + * taskInRecents is present, it will prioritize re-launching an existing instance via * {@link ActivityManagerWrapper#startActivityFromRecents(int, ActivityOptions)} */ private void startItemInfoActivity(ItemInfo info, @Nullable Task taskInRecents) { @@ -1307,26 +1825,29 @@ public class TaskbarActivityContext extends BaseTaskbarContext { .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon"); - if (info.user.equals(Process.myUserHandle())) { - // TODO(b/216683257): Use startActivityForResult for search results that require - // it. - if (taskInRecents != null) { - // Re launch instance from recents - ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info); - opts.options.setLaunchDisplayId( - getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId()); - if (ActivityManagerWrapper.getInstance() - .startActivityFromRecents(taskInRecents.key, opts.options)) { - mControllers.uiController.getRecentsView() - .addSideTaskLaunchCallback(opts.onEndCallback); - return; - } - } - - startActivity(intent); - } else { + if (!info.user.equals(Process.myUserHandle())) { + // TODO b/376819104: support Desktop launch animations for apps in managed profiles getSystemService(LauncherApps.class).startMainActivity( intent.getComponent(), info.user, intent.getSourceBounds(), null); + return; + } + int displayId = getDisplayId(); + // TODO(b/216683257): Use startActivityForResult for search results that require it. + if (taskInRecents != null) { + // Re launch instance from recents + ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info); + opts.options.setLaunchDisplayId(displayId); + if (ActivityManagerWrapper.getInstance() + .startActivityFromRecents(taskInRecents.key, opts.options)) { + mControllers.uiController.getRecentsView() + .addSideTaskLaunchCallback(opts.onEndCallback); + return; + } + } + if (shouldLaunchInDesktop(displayId, info)) { + launchDesktopApp(intent, info, displayId); + } else { + startActivity(intent, null); } } catch (NullPointerException | ActivityNotFoundException | SecurityException e) { Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT) @@ -1335,6 +1856,46 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } } + private boolean shouldLaunchInDesktop(int displayId, ItemInfo info) { + if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue()) { + return false; + } + if (DesktopExperienceFlags.ENABLE_DESKTOP_FIRST_FULLSCREEN_REFOCUS_BUGFIX.isTrue() + && DisplayController.isInDesktopFirstMode(this) + && mControllers.taskbarRecentAppsController.hasSingleTask(info)) { + // Keep the fullscreen mode in desktop-first mode. + return false; + } + // Always launch in freeform if in external display. + return (DesktopExperienceFlags.ENABLE_FREEFORM_DISPLAY_LAUNCH_PARAMS.isTrue() + && isExternalDisplay(displayId)) || isTaskbarShowingDesktopTasks(); + } + + private void launchDesktopApp(Intent intent, ItemInfo info, int displayId) { + TaskbarRecentAppsController.TaskState taskState = + mControllers.taskbarRecentAppsController.getDesktopItemState(info); + RunningAppState appState = taskState.getRunningAppState(); + if (appState == RunningAppState.RUNNING || appState == RunningAppState.MINIMIZED) { + // We only need a custom animation (a RemoteTransition) if the task is minimized - if + // it's already visible it will just be brought forward. + RemoteTransition remoteTransition = (appState == RunningAppState.MINIMIZED) + ? createDesktopAppLaunchRemoteTransition( + AppLaunchType.UNMINIMIZE, Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON) + : null; + UI_HELPER_EXECUTOR.execute(() -> + SystemUiProxy.INSTANCE.get(this).showDesktopApp(taskState.getTaskId(), + remoteTransition, DesktopTaskToFrontReason.TASKBAR_TAP)); + return; + } + // There is no task associated with this launch - launch a new task through an intent + ActivityOptionsWrapper opts = getActivityLaunchDesktopOptions(); + if (DesktopModeFlags.ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX.isTrue()) { + mSysUiProxy.startLaunchIntentTransition(intent, opts.options.toBundle(), displayId); + } else { + startActivity(intent, opts.options.toBundle()); + } + } + /** Expands a folder icon when it is clicked */ private void expandFolder(FolderIcon folderIcon) { Folder folder = folderIcon.getFolder(); @@ -1360,13 +1921,19 @@ public class TaskbarActivityContext extends BaseTaskbarContext { folder.animateOpen(); getStatsLogManager().logger().withItemInfo(folder.mInfo).log(LAUNCHER_FOLDER_OPEN); - folder.iterateOverItems((itemInfo, itemView) -> { + folder.mapOverItems((itemInfo, itemView) -> { mControllers.taskbarViewController .setClickAndLongClickListenersForIcon(itemView); // To play haptic when dragging, like other Taskbar items do. itemView.setHapticFeedbackEnabled(true); return false; }); + + // Close any open taskbar tooltips. + if (AbstractFloatingView.hasOpenView(this, TYPE_ON_BOARD_POPUP)) { + AbstractFloatingView.getOpenView(this, TYPE_ON_BOARD_POPUP) + .close(/* animate= */ false); + } }); } @@ -1379,10 +1946,19 @@ public class TaskbarActivityContext extends BaseTaskbarContext { /** * Called when we want to unstash taskbar when user performs swipes up gesture. + * + * @param delayTaskbarBackground whether we will delay the taskbar background animation */ - public void onSwipeToUnstashTaskbar() { + public void onSwipeToUnstashTaskbar(boolean delayTaskbarBackground) { + mControllers.uiController.onSwipeToUnstashTaskbar(); + boolean wasStashed = mControllers.taskbarStashController.isStashed(); - mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false); + if (isTransientTaskbar()) { + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false, + SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE, delayTaskbarBackground); + } else if (shouldAllowTaskbarToAutoStash()) { + mControllers.taskbarStashController.updateAndAnimatePinnedTaskbar(false); + } boolean isStashed = mControllers.taskbarStashController.isStashed(); if (isStashed != wasStashed) { VibratorWrapper.INSTANCE.get(this).vibrateForTaskbarUnstash(); @@ -1390,15 +1966,6 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.taskbarEduTooltipController.hide(); } - /** - * Called when we want to open bubblebar when user performs swipes up gesture. - */ - public void onSwipeToOpenBubblebar() { - mControllers.bubbleControllers.ifPresent(controllers -> { - controllers.bubbleStashController.showBubbleBar(/* expandBubbles= */ true); - }); - } - /** Returns {@code true} if Taskbar All Apps is open. */ public boolean isTaskbarAllAppsOpen() { return mControllers.taskbarAllAppsController.isOpen(); @@ -1417,8 +1984,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } /** - * Called to start the taskbar translation spring to its settled translation - * (0). + * Called to start the taskbar translation spring to its settled translation (0). */ public void startTranslationSpring() { mControllers.taskbarTranslationController.startSpring(); @@ -1439,19 +2005,23 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } /** - * Called when we detect a motion down or up/cancel in the nav region while - * stashed. + * Called when we detect a motion down or up/cancel in the nav region while stashed. * - * @param animateForward Whether to animate towards the unstashed hint state or - * back to stashed. + * @param animateForward Whether to animate towards the unstashed hint state or back to stashed. */ public void startTaskbarUnstashHint(boolean animateForward) { mControllers.taskbarStashController.startUnstashHint(animateForward); } /** - * Enables the auto timeout for taskbar stashing. This method should only be - * used for taskbar + * @return if we should allow taskbar to auto stash + */ + public boolean shouldAllowTaskbarToAutoStash() { + return mControllers.taskbarStashController.shouldAllowTaskbarToAutoStash(); + } + + /** + * Enables the auto timeout for taskbar stashing. This method should only be used for taskbar * testing. */ @VisibleForTesting @@ -1464,7 +2034,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { */ @VisibleForTesting public void unstashTaskbarIfStashed() { - if (DisplayController.isTransientTaskbar(this)) { + if (isTransientTaskbar()) { mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false); } } @@ -1483,32 +2053,40 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mIsUserSetupComplete; } + /** + * Checks if the simple view mode is enabled. + * + * Since Simple View puts the device in 3 button nav mode, we use that as a precursor to + * checking the actual value in Settings to avoid extra calls to Settings. + */ + public boolean isSimpleViewEnabled() { + return isThreeButtonNav() + && Settings.Secure.getInt(getContentResolver(), SIMPLE_VIEW_SETTINGS_KEY, 0) + > 0; + } + public boolean isNavBarKidsModeActive() { return mIsNavBarKidsMode && isThreeButtonNav(); } - protected boolean isNavBarForceVisible() { + @VisibleForTesting(otherwise = PROTECTED) + public boolean isNavBarForceVisible() { return mIsNavBarForceVisible; } /** * Displays a single frame of the Launcher start from SUW animation. * - * This animation is a combination of the Launcher resume animation, which - * animates the hotseat - * icons into position, the Taskbar unstash to hotseat animation, which animates - * the Taskbar - * stash bar into the hotseat icons, and an override to prevent showing the - * Taskbar all apps + * This animation is a combination of the Launcher resume animation, which animates the hotseat + * icons into position, the Taskbar unstash to hotseat animation, which animates the Taskbar + * stash bar into the hotseat icons, and an override to prevent showing the Taskbar all apps * button. * - * This should be used to run a Taskbar unstash to hotseat animation whose - * progress matches a + * This should be used to run a Taskbar unstash to hotseat animation whose progress matches a * swipe progress. * * @param duration a placeholder duration to be used to ensure all full-length - * sub-animations are properly coordinated. This duration should - * not actually + * sub-animations are properly coordinated. This duration should not actually * be used since this animation tracks a swipe progress. */ protected AnimatorPlaybackController createLauncherStartFromSuwAnim(int duration) { @@ -1524,41 +2102,29 @@ public class TaskbarActivityContext extends BaseTaskbarContext { duration); View allAppsButton = mControllers.taskbarViewController.getAllAppsButtonView(); - if (allAppsButton != null && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get()) { + if (!FeatureFlags.enableAllAppsButtonInHotseat()) { ValueAnimator alphaOverride = ValueAnimator.ofFloat(0, 1); alphaOverride.setDuration(duration); alphaOverride.addUpdateListener(a -> { // Override the alpha updates in the icon alignment animation. allAppsButton.setAlpha(0); }); + alphaOverride.addListener(AnimatorListeners.forSuccessCallback( + () -> allAppsButton.setAlpha(1f))); fullAnimation.play(alphaOverride); } return AnimatorPlaybackController.wrap(fullAnimation, duration); } - /** - * Called when we determine the touchable region. - * - * @param exclude {@code true} then the magnification region computation will - * omit the window. - */ - public void excludeFromMagnificationRegion(boolean exclude) { - if (mIsExcludeFromMagnificationRegion == exclude) { - return; - } - - mIsExcludeFromMagnificationRegion = exclude; - if (exclude) { - mWindowLayoutParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; - } else { - mWindowLayoutParams.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; - } - notifyUpdateLayoutParams(); - } - void notifyUpdateLayoutParams() { if (mDragLayer.isAttachedToWindow()) { + // Copy the current windowLayoutParams to mLastUpdatedLayoutParams and compare the diff. + // If there is no change, we will skip the call to updateViewLayout. + int changes = mLastUpdatedLayoutParams.copyFrom(mWindowLayoutParams); + if (changes == 0) { + return; + } if (enableTaskbarNoRecreate()) { mWindowManager.updateViewLayout(mDragLayer.getRootView(), mWindowLayoutParams); } else { @@ -1580,10 +2146,22 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mControllers.taskbarStashController.isInApp(); } + public boolean isInOverview() { + return mControllers.taskbarStashController.isInOverview(); + } + public boolean isInStashedLauncherState() { return mControllers.taskbarStashController.isInStashedLauncherState(); } + public TaskbarFeatureEvaluator getTaskbarFeatureEvaluator() { + return mTaskbarFeatureEvaluator; + } + + public TaskbarSpecsEvaluator getTaskbarSpecsEvaluator() { + return mTaskbarSpecsEvaluator; + } + protected void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "TaskbarActivityContext:"); @@ -1595,8 +2173,6 @@ public class TaskbarActivityContext extends BaseTaskbarContext { "%s\tmIsUserSetupComplete=%b", prefix, mIsUserSetupComplete)); pw.println(String.format( "%s\tmWindowLayoutParams.height=%dpx", prefix, mWindowLayoutParams.height)); - pw.println(String.format( - "%s\tmBindInProgress=%b", prefix, mBindingItems)); mControllers.dumpLogs(prefix + "\t", pw); mDeviceProfile.dump(this, prefix, pw); } @@ -1621,11 +2197,12 @@ public class TaskbarActivityContext extends BaseTaskbarContext { mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false); } - boolean canToggleHomeAllApps() { - return mControllers.uiController.canToggleHomeAllApps(); + boolean isIconAlignedWithHotseat() { + return mControllers.uiController.isIconAlignedWithHotseat(); } - @VisibleForTesting + // TODO(b/395061396): Remove `otherwise` when overview in widow is enabled. + @VisibleForTesting(otherwise = PACKAGE_PRIVATE) public TaskbarControllers getControllers() { return mControllers; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java index b9136e9f0f..03eef098dc 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java @@ -49,6 +49,12 @@ public class TaskbarAutohideSuspendController implements public static final int FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR = 1 << 5; // User has hovered the taskbar. public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6; + // User has multi instance window open. + public static final int FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN = 1 << 7; + // User has taskbar overflow open. + public static final int FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW = 1 << 8; + // Growth Framework nudge overlay is open above the Taskbar. + public static final int FLAG_AUTOHIDE_SUSPEND_GROWTH_NUDGE_OPEN = 1 << 9; @IntDef(flag = true, value = { FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, @@ -58,6 +64,9 @@ public class TaskbarAutohideSuspendController implements FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR, FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, + FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, + FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW, + FLAG_AUTOHIDE_SUSPEND_GROWTH_NUDGE_OPEN, }) @Retention(RetentionPolicy.SOURCE) public @interface AutohideSuspendFlag { @@ -138,6 +147,12 @@ public class TaskbarAutohideSuspendController implements "FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER"); appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR, "FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, + "FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW, + "FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW"); + appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_GROWTH_NUDGE_OPEN, + "FLAG_AUTOHIDE_SUSPEND_GROWTH_NUDGE_OPEN"); return str.toString(); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt index 2737cbd467..bd33177bfa 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt @@ -23,6 +23,7 @@ import android.graphics.Paint import android.graphics.Path import android.graphics.RectF import com.android.app.animation.Interpolators +import com.android.launcher3.Flags import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.Utilities.mapRange @@ -30,7 +31,7 @@ import com.android.launcher3.Utilities.mapToRange import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_PERSISTENT import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_TRANSIENT -import com.android.launcher3.util.DisplayController +import com.android.launcher3.taskbar.Utilities.getShapedTaskbarRadius import kotlin.math.min /** Helps draw the taskbar background, made up of a rectangle plus two inverted rounded corners. */ @@ -39,24 +40,27 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { private val isInSetup: Boolean = !context.isUserSetupComplete private val maxTransientTaskbarHeight = - context.transientTaskbarDeviceProfile.taskbarHeight.toFloat() + context.transientTaskbarDeviceProfile.taskbarProfile.height.toFloat() private val maxPersistentTaskbarHeight = - context.persistentTaskbarDeviceProfile.taskbarHeight.toFloat() + context.persistentTaskbarDeviceProfile.taskbarProfile.height.toFloat() var backgroundProgress = - if (DisplayController.isTransientTaskbar(context)) { + if (context.isTransientTaskbar) { PINNING_TRANSIENT } else { PINNING_PERSISTENT } var isAnimatingPinning = false + var isAnimatingPersistentTaskbar = false + var isAnimatingTransientTaskbar = false val paint = Paint() private val strokePaint = Paint() val lastDrawnTransientRect = RectF() - var backgroundHeight = context.deviceProfile.taskbarHeight.toFloat() + var backgroundHeight = context.deviceProfile.taskbarProfile.height.toFloat() var translationYForSwipe = 0f var translationYForStash = 0f + var translationXForBubbleBar = 0f private val transientBackgroundBounds = context.transientTaskbarBounds @@ -66,8 +70,8 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { private var keyShadowDistance = 0f private var bottomMargin = 0 - private val fullCornerRadius = context.cornerRadius.toFloat() - private var cornerRadius = fullCornerRadius + private val fullCornerRadius: Float + private var cornerRadius = 0f private var widthInsetPercentage = 0f private val square = Path() private val circle = Path() @@ -97,13 +101,17 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { shadowAlpha = LIGHT_THEME_SHADOW_ALPHA } - setCornerRoundness(DEFAULT_ROUNDNESS) + fullCornerRadius = context.cornerRadius.toFloat() + cornerRadius = fullCornerRadius + if (!context.isInDesktopMode()) { + setCornerRoundness(MAX_ROUNDNESS) + } } fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) { stashedHandleWidth = res.getDimensionPixelSize( - if (context.isPhoneMode || context.isTinyTaskbar) { + if (context.isPhoneMode || context.isTinyTaskbar || context.isBubbleBarOnPhone) { R.dimen.taskbar_stashed_small_screen } else { R.dimen.taskbar_stashed_handle_width @@ -117,7 +125,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { * @param cornerRoundness 0 has no round corner, 1 has complete round corner. */ fun setCornerRoundness(cornerRoundness: Float) { - if (DisplayController.isTransientTaskbar(context) && !transientBackgroundBounds.isEmpty) { + if (context.isTransientTaskbar && !transientBackgroundBounds.isEmpty) { return } @@ -139,7 +147,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { /** Draws the background with the given paint and height, on the provided canvas. */ fun draw(canvas: Canvas) { if (isInSetup) return - val isTransientTaskbar = backgroundProgress == 0f + val isTransientTaskbar = context.isTransientTaskbar canvas.save() if (!isTransientTaskbar || transientBackgroundBounds.isEmpty || isAnimatingPinning) { drawPersistentBackground(canvas) @@ -153,7 +161,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { } private fun drawPersistentBackground(canvas: Canvas) { - if (isAnimatingPinning) { + if (isAnimatingPinning || isAnimatingPersistentTaskbar) { val persistentTaskbarHeight = maxPersistentTaskbarHeight * backgroundProgress canvas.translate(0f, canvas.height - persistentTaskbarHeight) // Draw the background behind taskbar content. @@ -176,19 +184,20 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { private fun drawTransientBackground(canvas: Canvas) { val res = context.resources val transientTaskbarHeight = maxTransientTaskbarHeight * (1f - backgroundProgress) + val isAnimating = isAnimatingPinning || isAnimatingTransientTaskbar val heightProgressWhileAnimating = - if (isAnimatingPinning) transientTaskbarHeight else backgroundHeight + if (isAnimating) transientTaskbarHeight else backgroundHeight var progress = heightProgressWhileAnimating / maxTransientTaskbarHeight progress = Math.round(progress * 100f) / 100f - if (isAnimatingPinning) { + if (isAnimating) { var scale = transientTaskbarHeight / maxTransientTaskbarHeight scale = Math.round(scale * 100f) / 100f bottomMargin = mapRange( scale, 0f, - res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat() + res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat(), ) .toInt() shadowBlur = @@ -215,7 +224,12 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { val newWidth = mapRange(progress, backgroundWidthWhileAnimating, fullWidth.toFloat()) val halfWidthDelta = (fullWidth - newWidth) / 2f - val radius = newBackgroundHeight / 2f + val radius = + if (Flags.enableLauncherIconShapes()) { + getShapedTaskbarRadius(context) + } else { + newBackgroundHeight / 2f + } val bottomMarginProgress = bottomMargin * ((1f - progress) / 2f) // Aligns the bottom with the bottom of the stashed handle. @@ -227,7 +241,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { -mapRange( 1f - progress, 0f, - if (isAnimatingPinning) 0f else stashedHandleHeight / 2f + if (isAnimatingPinning) 0f else stashedHandleHeight / 2f, ) // Draw shadow. @@ -237,15 +251,15 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { shadowBlur, 0f, keyShadowDistance, - setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)) + setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)), ) strokePaint.alpha = (paint.alpha * strokeAlpha) / 255 - + val currentTranslationX = translationXForBubbleBar * progress lastDrawnTransientRect.set( - transientBackgroundBounds.left + halfWidthDelta, + transientBackgroundBounds.left + halfWidthDelta + currentTranslationX, bottom - newBackgroundHeight, - transientBackgroundBounds.right - halfWidthDelta, - bottom + transientBackgroundBounds.right - halfWidthDelta + currentTranslationX, + bottom, ) val horizontalInset = fullWidth * widthInsetPercentage lastDrawnTransientRect.inset(horizontalInset, 0f) @@ -263,7 +277,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) { } companion object { - const val DEFAULT_ROUNDNESS = 1f + const val MAX_ROUNDNESS = 1f private const val DARK_THEME_STROKE_ALPHA = 51 private const val LIGHT_THEME_STROKE_ALPHA = 41 private const val DARK_THEME_SHADOW_ALPHA = 51f diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java index 58c5e835c9..f24bd28330 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.taskbar; +import android.animation.AnimatorSet; import android.content.pm.ActivityInfo.Config; import androidx.annotation.NonNull; @@ -24,8 +25,10 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController; import com.android.launcher3.taskbar.bubbles.BubbleControllers; +import com.android.launcher3.taskbar.growth.NudgeController; import com.android.launcher3.taskbar.overlay.TaskbarOverlayController; import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.ArrayList; @@ -64,6 +67,9 @@ public class TaskbarControllers { public final KeyboardQuickSwitchController keyboardQuickSwitchController; public final TaskbarPinningController taskbarPinningController; public final Optional bubbleControllers; + public final TaskbarDesktopModeController taskbarDesktopModeController; + public final NudgeController nudgeController; + public final NudgeViewController nudgeViewController; @Nullable private LoggableTaskbarController[] mControllersToLog = null; @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null; @@ -111,7 +117,10 @@ public class TaskbarControllers { TaskbarEduTooltipController taskbarEduTooltipController, KeyboardQuickSwitchController keyboardQuickSwitchController, TaskbarPinningController taskbarPinningController, - Optional bubbleControllers) { + Optional bubbleControllers, + TaskbarDesktopModeController taskbarDesktopModeController, + NudgeController nudgeController, + NudgeViewController nudgeViewController) { this.taskbarActivityContext = taskbarActivityContext; this.taskbarDragController = taskbarDragController; this.navButtonController = navButtonController; @@ -138,6 +147,9 @@ public class TaskbarControllers { this.keyboardQuickSwitchController = keyboardQuickSwitchController; this.taskbarPinningController = taskbarPinningController; this.bubbleControllers = bubbleControllers; + this.taskbarDesktopModeController = taskbarDesktopModeController; + this.nudgeController = nudgeController; + this.nudgeViewController = nudgeViewController; } /** @@ -145,15 +157,15 @@ public class TaskbarControllers { * TaskbarControllers instance, but should be careful to only access things that were created * in constructors for now, as some controllers may still be waiting for init(). */ - public void init(@NonNull TaskbarSharedState sharedState) { + public void init(@NonNull TaskbarSharedState sharedState, AnimatorSet startAnimation) { mAreAllControllersInitialized = false; mSharedState = sharedState; taskbarDragController.init(this); navbarButtonsViewController.init(this); rotationButtonController.init(); - taskbarDragLayerController.init(this); - taskbarViewController.init(this); + taskbarDragLayerController.init(this, startAnimation); + taskbarViewController.init(this, startAnimation); taskbarScrimViewController.init(this); taskbarUnfoldAnimationController.init(this); taskbarKeyguardController.init(navbarButtonsViewController); @@ -165,14 +177,17 @@ public class TaskbarControllers { taskbarOverlayController.init(this); taskbarAllAppsController.init(this, sharedState.allAppsVisible); navButtonController.init(this); + bubbleControllers.ifPresentOrElse(controllers -> controllers.init(sharedState, this), + sharedState::clearBubbleData); taskbarInsetsController.init(this); voiceInteractionWindowController.init(this); - taskbarRecentAppsController.init(this); + taskbarRecentAppsController.init(this, sharedState.recentTasksBeforeTaskbarRecreate); taskbarTranslationController.init(this); taskbarEduTooltipController.init(this); keyboardQuickSwitchController.init(this); taskbarPinningController.init(this, mSharedState); - bubbleControllers.ifPresent(controllers -> controllers.init(this)); + taskbarDesktopModeController.init(this, mSharedState); + nudgeController.init(this); mControllersToLog = new LoggableTaskbarController[] { taskbarDragController, navButtonController, navbarButtonsViewController, @@ -183,13 +198,29 @@ public class TaskbarControllers { voiceInteractionWindowController, taskbarRecentAppsController, taskbarTranslationController, taskbarEduTooltipController, keyboardQuickSwitchController, taskbarPinningController, + nudgeController }; mBackgroundRendererControllers = new BackgroundRendererController[] { taskbarDragLayerController, taskbarScrimViewController, voiceInteractionWindowController }; - mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS); + // TODO(b/401061748): get primary status from + // TaskbarDesktopModeController/DesktopVisibilityController. + if (taskbarDesktopModeController.isInDesktopModeAndNotInOverview( + taskbarActivityContext.getDisplayId()) + || !taskbarActivityContext.isPrimaryDisplay()) { + mCornerRoundness.value = taskbarDesktopModeController.getTaskbarCornerRoundness( + mSharedState.showCornerRadiusInDesktopMode); + } else { + mCornerRoundness.value = TaskbarBackgroundRenderer.MAX_ROUNDNESS; + } + updateCornerRoundness(); + onPostInit(); + } + + @VisibleForTesting + public void onPostInit() { mAreAllControllersInitialized = true; for (Runnable postInitCallback : mPostInitCallbacks) { postInitCallback.run(); @@ -205,9 +236,20 @@ public class TaskbarControllers { uiController = newUiController; uiController.init(this); uiController.updateStateForSysuiFlags(mSharedState.sysuiStateFlags); - + // if bubble controllers are present configure the UI controller + bubbleControllers.ifPresentOrElse(bubbleControllers -> { + BubbleBarLocation location = + bubbleControllers.bubbleBarViewController.getBubbleBarLocation(); + boolean hiddenForBubbles = + bubbleControllers.bubbleBarViewController.isHiddenForNoBubbles(); + if (!hiddenForBubbles) { + uiController.adjustHotseatForBubbleBar(/* isBubbleBarVisible= */ true); + } + uiController.onBubbleBarLocationUpdated(location); + }, () -> uiController.onBubbleBarLocationUpdated(null)); // Notify that the ui controller has changed navbarButtonsViewController.onUiControllerChanged(); + taskbarViewController.onUiControllerChanged(); } @Nullable @@ -227,8 +269,8 @@ public class TaskbarControllers { */ public void onDestroy() { mAreAllControllersInitialized = false; - mSharedState = null; + taskbarDragController.onDestroy(); navbarButtonsViewController.onDestroy(); uiController.onDestroy(); rotationButtonController.onDestroy(); @@ -236,6 +278,7 @@ public class TaskbarControllers { taskbarUnfoldAnimationController.onDestroy(); taskbarViewController.onDestroy(); stashedHandleViewController.onDestroy(); + nudgeViewController.onDestroy(); taskbarAutohideSuspendController.onDestroy(); taskbarPopupController.onDestroy(); taskbarForceVisibleImmersiveController.onDestroy(); @@ -248,9 +291,10 @@ public class TaskbarControllers { keyboardQuickSwitchController.onDestroy(); taskbarStashController.onDestroy(); bubbleControllers.ifPresent(controllers -> controllers.onDestroy()); - + taskbarDesktopModeController.onDestroy(); mControllersToLog = null; mBackgroundRendererControllers = null; + mSharedState = null; } /** @@ -282,6 +326,11 @@ public class TaskbarControllers { } uiController.dumpLogs(prefix + "\t", pw); rotationButtonController.dumpLogs(prefix + "\t", pw); + if (bubbleControllers.isPresent()) { + bubbleControllers.get().dump(pw); + } else { + pw.println(String.format("%s\t%s", prefix, "Bubble controllers are empty.")); + } } /** @@ -307,7 +356,7 @@ public class TaskbarControllers { return taskbarActivityContext; } - protected interface LoggableTaskbarController { + public interface LoggableTaskbarController { void dumpLogs(String prefix, PrintWriter pw); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopExperienceFlags.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopExperienceFlags.kt new file mode 100644 index 0000000000..fec1a08a8a --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopExperienceFlags.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.window.DesktopExperienceFlags.DesktopExperienceFlag +import com.android.launcher3.Flags + +object TaskbarDesktopExperienceFlags { + @JvmField + val enableAltTabKqsOnConnectedDisplays: DesktopExperienceFlag = + DesktopExperienceFlag( + Flags::enableAltTabKqsOnConnectedDisplays, + /* shouldOverrideByDevOption= */ true, + Flags.FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS, + ) + + @JvmField + val enableAltTabKqsFlatenning: DesktopExperienceFlag = + DesktopExperienceFlag( + Flags::enableAltTabKqsFlatenning, + /* shouldOverrideByDevOption= */ true, + Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING, + ) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt new file mode 100644 index 0000000000..ab502451ac --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt @@ -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.launcher3.taskbar + +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.statehandlers.DesktopVisibilityController.TaskbarDesktopModeListener +import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS + +/** Handles Taskbar in Desktop Windowing mode. */ +class TaskbarDesktopModeController( + private val taskbarActivityContext: TaskbarActivityContext, + private val desktopVisibilityController: DesktopVisibilityController, +) : TaskbarDesktopModeListener { + private lateinit var taskbarControllers: TaskbarControllers + private lateinit var taskbarSharedState: TaskbarSharedState + + val isLauncherAnimationRunning: Boolean + get() = desktopVisibilityController.launcherAnimationRunning + + fun init(controllers: TaskbarControllers, sharedState: TaskbarSharedState) { + taskbarControllers = controllers + taskbarSharedState = sharedState + desktopVisibilityController.registerTaskbarDesktopModeListener(this) + } + + fun isInDesktopMode(displayId: Int) = desktopVisibilityController.isInDesktopMode(displayId) + + fun isInDesktopModeAndNotInOverview(displayId: Int) = + desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId) + + override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) { + if (taskbarControllers.taskbarActivityContext.isDestroyed) return + taskbarSharedState.showCornerRadiusInDesktopMode = doesAnyTaskRequireTaskbarRounding + val cornerRadius = getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding) + taskbarControllers.taskbarCornerRoundness.animateToValue(cornerRadius).start() + } + + fun shouldShowDesktopTasksInTaskbar(): Boolean { + return shouldShowDesktopTasksInTaskbar(taskbarActivityContext.displayId) + } + + fun shouldShowDesktopTasksInTaskbar(displayId: Int): Boolean { + return isInDesktopMode(displayId) || + taskbarActivityContext.showDesktopTaskbarForFreeformDisplay() || + (taskbarActivityContext.showLockedTaskbarOnHome() && + taskbarControllers.taskbarStashController.isOnHome) + } + + fun getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding: Boolean): Float { + return if (doesAnyTaskRequireTaskbarRounding) { + MAX_ROUNDNESS + } else { + 0f + } + } + + fun onDestroy() = desktopVisibilityController.unregisterTaskbarDesktopModeListener(this) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt index a635537907..3ae06abea4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt @@ -29,23 +29,25 @@ import android.view.MotionEvent import android.view.View import android.widget.LinearLayout import android.widget.Switch +import androidx.core.content.res.ResourcesCompat import androidx.core.view.postDelayed import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.launcher3.Flags +import com.android.launcher3.LauncherPrefs import com.android.launcher3.R import com.android.launcher3.popup.ArrowPopup import com.android.launcher3.popup.RoundedArrowDrawable -import com.android.launcher3.util.DisplayController import com.android.launcher3.util.Themes import com.android.launcher3.views.ActivityContext +import com.android.wm.shell.Flags.enableGsf +import kotlin.math.max +import kotlin.math.min /** Popup view with arrow for taskbar pinning */ class TaskbarDividerPopupView @JvmOverloads -constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, -) : ArrowPopup(context, attrs, defStyleAttr) { +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + ArrowPopup(context, attrs, defStyleAttr) { companion object { private const val TAG = "TaskbarDividerPopupView" private const val DIVIDER_POPUP_CLOSING_DELAY = 333L @@ -55,26 +57,38 @@ constructor( fun createAndPopulate( view: View, taskbarActivityContext: TaskbarActivityContext, + horizontalPosition: Float, ): TaskbarDividerPopupView<*> { val taskMenuViewWithArrow = taskbarActivityContext.layoutInflater.inflate( R.layout.taskbar_divider_popup_menu, taskbarActivityContext.dragLayer, - false + false, ) as TaskbarDividerPopupView<*> - return taskMenuViewWithArrow.populateForView(view) + return taskMenuViewWithArrow.populateForView(view, horizontalPosition) } } private lateinit var dividerView: View + private var horizontalPosition = 0.0f + private val taskbarActivityContext: TaskbarActivityContext = + ActivityContext.lookupContext(context) private val popupCornerRadius = Themes.getDialogCornerRadius(context) private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height) private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius) + private val minPaddingFromScreenEdge = + resources.getDimension(R.dimen.taskbar_pinning_popup_menu_min_padding_from_screen_edge) + + private var alwaysShowTaskbarOn = + if (taskbarActivityContext.isTaskbarShowingDesktopTasks) { + LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE.get(context) + } else { + !taskbarActivityContext.isTransientTaskbar + } - private var alwaysShowTaskbarOn = !DisplayController.isTransientTaskbar(context) private var didPreferenceChange = false private var verticalOffsetForPopupView = resources.getDimensionPixelSize(R.dimen.taskbar_pinning_popup_menu_vertical_margin) @@ -103,13 +117,22 @@ constructor( val alwaysShowTaskbarSwitch = requireViewById(R.id.taskbar_pinning_switch) val taskbarVisibilityIcon = requireViewById(R.id.taskbar_pinning_visibility_icon) + if (enableGsf()) { + taskbarVisibilityIcon.background = + ResourcesCompat.getDrawable( + context.resources, + R.drawable.ic_visibility_filled, + context.theme, + ) + } + alwaysShowTaskbarSwitch.isChecked = alwaysShowTaskbarOn alwaysShowTaskbarSwitch.setOnTouchListener { view, event -> (view.parent as View).onTouchEvent(event) } alwaysShowTaskbarSwitch.setOnClickListener { view -> (view.parent as View).performClick() } - if (ActivityContext.lookupContext(context).isGestureNav) { + if (taskbarActivityContext.isGestureNav) { taskbarSwitchOption.setOnClickListener { alwaysShowTaskbarSwitch.isChecked = !alwaysShowTaskbarOn onClickAlwaysShowTaskbarSwitchOption() @@ -128,7 +151,36 @@ constructor( /** Orient object as usual and then center object horizontally. */ override fun orientAboutObject() { super.orientAboutObject() - x = mTempRect.centerX() - measuredWidth / 2f + x = + if (Flags.showTaskbarPinningPopupFromAnywhere()) { + val xForCenterAlignment = horizontalPosition - measuredWidth / 2f + val maxX = popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge + when { + // Left-aligned popup and its arrow pointing to the event position if there is + // not enough space to center it. + xForCenterAlignment < minPaddingFromScreenEdge -> + max( + minPaddingFromScreenEdge, + horizontalPosition - mArrowOffsetHorizontal - mArrowWidth / 2, + ) + + // Right-aligned popup and its arrow pointing to the event position if there + // is not enough space to center it. + xForCenterAlignment > maxX -> + min( + horizontalPosition - measuredWidth + + mArrowOffsetHorizontal + + mArrowWidth / 2, + popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge, + ) + + // Default alignment where the popup and its arrow are centered relative to the + // event position. + else -> xForCenterAlignment + } + } else { + mTempRect.centerX() - measuredWidth / 2f + } } override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { @@ -142,8 +194,9 @@ constructor( return false } - private fun populateForView(view: View): TaskbarDividerPopupView<*> { + private fun populateForView(view: View, horizontalPosition: Float): TaskbarDividerPopupView<*> { dividerView = view + this@TaskbarDividerPopupView.horizontalPosition = horizontalPosition tryUpdateBackground() return this } @@ -169,12 +222,31 @@ constructor( override fun addArrow() { super.addArrow() - // Change arrow location to the middle of popup. - mArrow.x = (dividerView.x + dividerView.width / 2) - (mArrowWidth / 2) + if (Flags.showTaskbarPinningPopupFromAnywhere()) { + mArrow.x = + min( + max( + minPaddingFromScreenEdge + mArrowOffsetHorizontal, + horizontalPosition - mArrowWidth / 2, + ), + popupContainer.getWidth() - + minPaddingFromScreenEdge - + mArrowOffsetHorizontal - + mArrowWidth, + ) + } else { + val location = IntArray(2) + popupContainer.getLocationInDragLayer(dividerView, location) + val dividerViewX = location[0].toFloat() + // Change arrow location to the middle of popup. + mArrow.x = (dividerViewX + dividerView.width / 2) - (mArrowWidth / 2) + } } override fun updateArrowColor() { - if (!Gravity.isVertical(mGravity)) { + if (Flags.showTaskbarPinningPopupFromAnywhere()) { + super.updateArrowColor() + } else if (!Gravity.isVertical(mGravity)) { mArrow.background = RoundedArrowDrawable( arrowWidth, @@ -195,8 +267,8 @@ constructor( } override fun getExtraVerticalOffset(): Int { - return (mActivityContext.deviceProfile.taskbarHeight - - mActivityContext.deviceProfile.taskbarIconSize) / 2 + verticalOffsetForPopupView + return (mActivityContext.deviceProfile.taskbarProfile.height - + mActivityContext.deviceProfile.taskbarProfile.iconSize) / 2 + verticalOffsetForPopupView } override fun onCreateCloseAnimation(anim: AnimatorSet?) { @@ -210,7 +282,7 @@ constructor( /** Aligning the view pivot to center for animation. */ override fun setPivotForOpenCloseAnimation() { - pivotX = measuredWidth / 2f + pivotX = mArrow.x + mArrowWidth / 2 - x pivotY = measuredHeight.toFloat() } @@ -224,13 +296,13 @@ constructor( ObjectAnimator.ofFloat( this, TRANSLATION_Y, - *floatArrayOf(this.translationY, this.translationY + translateYValue) + *floatArrayOf(this.translationY, this.translationY + translateYValue), ) val arrowTranslateY = ObjectAnimator.ofFloat( mArrow, TRANSLATION_Y, - *floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue) + *floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue), ) val animatorSet = AnimatorSet() animatorSet.playTogether(alpha, arrowAlpha, translateY, arrowTranslateY) @@ -240,7 +312,7 @@ constructor( private fun getAnimatorOfFloat( view: View, property: Property, - vararg values: Float + vararg values: Float, ): Animator { val animator: Animator = ObjectAnimator.ofFloat(view, property, *values) animator.setDuration(DIVIDER_POPUP_CLOSING_ANIMATION_DURATION) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java index 536daecdbf..eac264736e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java @@ -18,7 +18,7 @@ package com.android.launcher3.taskbar; import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS; -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION; import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.EXTENDED_CONTAINERS; @@ -34,6 +34,7 @@ import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Point; @@ -51,6 +52,7 @@ import android.view.ViewRootImpl; import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.app.animation.Interpolators; import com.android.internal.logging.InstanceId; @@ -66,6 +68,7 @@ import com.android.launcher3.dragndrop.DragDriver; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.dragndrop.DraggableView; +import com.android.launcher3.folder.Folder; import com.android.launcher3.graphics.DragPreviewProvider; import com.android.launcher3.logger.LauncherAtom.ContainerInfo; import com.android.launcher3.logging.StatsLogManager; @@ -74,18 +77,18 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; -import com.android.launcher3.statehandlers.DesktopVisibilityController; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.views.BubbleTextHolder; -import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.util.LogUtils; import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.SingleTask; import com.android.systemui.shared.recents.model.Task; -import com.android.wm.shell.draganddrop.DragAndDropConstants; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.draganddrop.DragAndDropConstants; import java.io.PrintWriter; import java.util.Arrays; @@ -93,8 +96,7 @@ import java.util.Collections; import java.util.function.Predicate; /** - * Handles long click on Taskbar items to start a system drag and drop - * operation. + * Handles long click on Taskbar items to start a system drag and drop operation. */ public class TaskbarDragController extends DragController implements TaskbarControllers.LoggableTaskbarController { @@ -114,6 +116,7 @@ public class TaskbarDragController extends DragController im private int mRegistrationY; private boolean mIsSystemDragInProgress; + private boolean mIsDropHandledByDropTarget; // Animation for the drag shadow back into position after an unsuccessful drag private ValueAnimator mReturnAnimator; @@ -128,6 +131,14 @@ public class TaskbarDragController extends DragController im public void init(TaskbarControllers controllers) { mControllers = controllers; + mControllers.runAfterInit(() -> mControllers.bubbleControllers.ifPresent( + c -> c.dragToBubbleController.addBubbleBarDropTargets(this))); + } + + /** Called when the controller is destroyed. */ + public void onDestroy() { + mControllers.bubbleControllers.ifPresent( + c -> c.dragToBubbleController.removeBubbleBarDropTargets(this)); } public void setDisallowGlobalDrag(boolean disallowGlobalDrag) { @@ -139,10 +150,8 @@ public class TaskbarDragController extends DragController im } /** - * Attempts to start a system drag and drop operation for the given View, using - * its tag to + * Attempts to start a system drag and drop operation for the given View, using its tag to * generate the ClipDescription and Intent. - * * @return Whether {@link View#startDragAndDrop} started successfully. */ public boolean startDragOnLongClick(View view) { @@ -184,7 +193,9 @@ public class TaskbarDragController extends DragController im private DragView startInternalDrag( BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) { - float iconScale = btv.getIcon().getAnimatedScale(); + // TODO(b/344038728): null check is only necessary because Recents doesn't use + // FastBitmapDrawable + float iconScale = btv.getIcon() == null ? 1f : btv.getIcon().getAnimatedScale(); // Clear the pressed state if necessary btv.clearFocus(); @@ -192,8 +203,7 @@ public class TaskbarDragController extends DragController im btv.clearPressedBackground(); final DragPreviewProvider previewProvider = dragPreviewProvider == null - ? new DragPreviewProvider(btv) - : dragPreviewProvider; + ? new DragPreviewProvider(btv) : dragPreviewProvider; final Drawable drawable = previewProvider.createDrawable(); final float scale = previewProvider.getScaleAndPosition(drawable, mTempXY); int dragLayerX = mTempXY[0]; @@ -205,12 +215,13 @@ public class TaskbarDragController extends DragController im DragOptions dragOptions = new DragOptions(); // First, see if view is a search result that needs custom pre-drag conditions. - dragOptions.preDragCondition = mControllers.taskbarAllAppsController.createPreDragConditionForSearch(btv); + dragOptions.preDragCondition = + mControllers.taskbarAllAppsController.createPreDragConditionForSearch(btv); if (dragOptions.preDragCondition == null) { // See if view supports a popup container. - PopupContainerWithArrow popupContainer = mControllers.taskbarPopupController - .showForIcon(btv); + PopupContainerWithArrow popupContainer = + mControllers.taskbarPopupController.showForIcon(btv); if (popupContainer != null) { dragOptions.preDragCondition = popupContainer.createPreDragCondition(false); } @@ -250,9 +261,9 @@ public class TaskbarDragController extends DragController im /* originalView = */ btv, dragLayerX + dragOffset.x, dragLayerY + dragOffset.y, - (View target, DropTarget.DragObject d, boolean success) -> { - } /* DragSource */, - (ItemInfo) btv.getTag(), + (View target, DropTarget.DragObject d, boolean success) -> + mIsDropHandledByDropTarget = success /* DragSource */, + btv.getTag() instanceof ItemInfo itemInfo ? itemInfo : null, dragRect, scale * iconScale, scale, @@ -292,21 +303,23 @@ public class TaskbarDragController extends DragController im initialDragViewScale, dragViewScaleOnDrop, scalePx); - dragView.setItemInfo(dragInfo); + if (dragInfo != null) { + dragView.setItemInfo(dragInfo); + } mDragObject.dragComplete = false; mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft); mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop); - mDragDriver = DragDriver.create(this, mOptions, /* secondaryEventConsumer = */ ev -> { - }); + mDragDriver = DragDriver.create(this, mOptions, /* secondaryEventConsumer = */ ev -> {}); if (!mOptions.isAccessibleDrag) { mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); } mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; - mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy(); + mDragObject.originalDragInfo = + mDragObject.dragInfo != null ? mDragObject.dragInfo.makeShallowCopy() : null; if (mOptions.preDragCondition != null) { dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0 @@ -333,8 +346,7 @@ public class TaskbarDragController extends DragController im /** Invoked when an animation running as part of pre-drag finishes. */ public void onPreDragAnimationEnd() { - // Drag might be cancelled during the DragView animation, so check mIsPreDrag - // again. + // Drag might be cancelled during the DragView animation, so check mIsPreDrag again. if (mIsInPreDrag) { callOnDragStart(); } @@ -344,12 +356,10 @@ public class TaskbarDragController extends DragController im protected void callOnDragStart() { super.callOnDragStart(); // TODO(297921594) clean it up when taskbar to desktop drag is implemented. - DesktopVisibilityController desktopController = LauncherActivityInterface.INSTANCE - .getDesktopVisibilityController(); - // Pre-drag has ended, start the global system drag. - if (mDisallowGlobalDrag || (desktopController != null - && desktopController.areDesktopTasksVisible())) { + if (mDisallowGlobalDrag + || mControllers.taskbarDesktopModeController + .isInDesktopModeAndNotInOverview(mActivity.getDisplayId())) { AbstractFloatingView.closeAllOpenViewsExcept(mActivity, TYPE_TASKBAR_ALL_APPS); return; } @@ -420,9 +430,12 @@ public class TaskbarDragController extends DragController im item.user)); intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage()); intent.putExtra(Intent.EXTRA_SHORTCUT_ID, deepShortcutId); + ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) item).getDeepShortcutInfo(); + if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && shortcutInfo != null) { + intent.putExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO, shortcutInfo); + } } else if (item.itemType == ITEM_TYPE_SEARCH_ACTION) { - // TODO(b/289261756): Buggy behavior when split opposite to an existing search - // pane. + // TODO(b/289261756): Buggy behavior when split opposite to an existing search pane. intent.putExtra( ClipDescription.EXTRA_PENDING_INTENT, PendingIntent.getActivityAsUser( @@ -438,8 +451,8 @@ public class TaskbarDragController extends DragController im null, item.user)); } intent.putExtra(Intent.EXTRA_USER, item.user); - } else if (tag instanceof Task) { - Task task = (Task) tag; + } else if (tag instanceof SingleTask singleTask) { + Task task = singleTask.getTask(); clipDescription = new ClipDescription(task.titleDescription, new String[] { ClipDescription.MIMETYPE_APPLICATION_TASK @@ -450,14 +463,14 @@ public class TaskbarDragController extends DragController im } if (clipDescription != null && intent != null) { - Pair instanceIds = LogUtils - .getShellShareableInstanceId(); + Pair instanceIds = + LogUtils.getShellShareableInstanceId(); // Need to share the same InstanceId between launcher3 and WM Shell (internal). InstanceId internalInstanceId = instanceIds.first; com.android.launcher3.logging.InstanceId launcherInstanceId = instanceIds.second; intent.putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, internalInstanceId); - if (DisplayController.isTransientTaskbar(mActivity)) { + if (mActivity.isTransientTaskbar()) { // Tell WM Shell to ignore drag events in the provided transient taskbar region. TaskbarDragLayer dragLayer = mControllers.taskbarActivityContext.getDragLayer(); int[] locationOnScreen = dragLayer.getLocationOnScreen(); @@ -497,6 +510,8 @@ public class TaskbarDragController extends DragController im } else { // This will take care of calling maybeOnDragEnd() after the animation animateGlobalDragViewToOriginalPosition(btv, dragEvent); + //TODO(b/399678274): hide drop target in shell + notifyBubbleBarItemDragCanceled(); } mActivity.getDragLayer().setOnDragListener(null); @@ -516,39 +531,61 @@ public class TaskbarDragController extends DragController im return mIsSystemDragInProgress; } + @VisibleForTesting private void maybeOnDragEnd() { if (!isDragging()) { ((BubbleTextView) mDragObject.originalView).setIconDisabled(false); mControllers.taskbarAutohideSuspendController.updateFlag( TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false); mActivity.onDragEnd(); + // If an item is dropped on the bubble bar, the bubble bar handles the drop, + // so it should not collapse along with the taskbar. + boolean droppedOnBubbleBar = notifyBubbleBarItemDropped(); if (mReturnAnimator == null) { // Upon successful drag, immediately stash taskbar. // Note, this must be done last to ensure no AutohideSuspendFlags are active, as // that will prevent us from stashing until the timeout. - mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); - + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar( + /* stash = */ true, + /* shouldBubblesFollow = */ !droppedOnBubbleBar + ); mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo) .log(LAUNCHER_APP_LAUNCH_DRAGDROP); } } } + /** + * Exits the Bubble Bar drop target mode if applicable. + * + * @return {@code true} if drop target mode was active. + */ + private boolean notifyBubbleBarItemDropped() { + return mControllers.bubbleControllers.map(bc -> { + BubbleBarViewController bubbleBarViewController = bc.bubbleBarViewController; + boolean showingDropTarget = bubbleBarViewController.isShowingDropTarget(); + if (showingDropTarget) { + bubbleBarViewController.onItemDragCompleted(); + } + return showingDropTarget; + }).orElse(false); + } + + private void notifyBubbleBarItemDragCanceled() { + mControllers.bubbleControllers.ifPresent(bc -> + bc.bubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone()); + } + @Override protected void endDrag() { - if (mDisallowGlobalDrag) { - // We need to explicitly set deferDragViewCleanupPostAnimation to true here so - // the - // super call doesn't remove it from the drag layer before the animation - // completes. + if (mDisallowGlobalDrag && !mIsDropHandledByDropTarget) { + // We need to explicitly set deferDragViewCleanupPostAnimation to true here so the + // super call doesn't remove it from the drag layer before the animation completes. // This variable gets set in to false in super.dispatchDropComplete() because it - // (rightfully so, perhaps) thinks this drag operation has failed, and does its - // own + // (rightfully so, perhaps) thinks this drag operation has failed, and does its own // internal cleanup. - // Another way to approach this would be to make all of overview a drop target - // and - // accept the drop as successful and then run the setupReturnDragAnimator to - // simulate + // Another way to approach this would be to make all of overview a drop target and + // accept the drop as successful and then run the setupReturnDragAnimator to simulate // drop failure to the user mDragObject.deferDragViewCleanupPostAnimation = true; @@ -633,8 +670,7 @@ public class TaskbarDragController extends DragController im syncGroup.add(viewRoot, null /* runnable */); syncGroup.addTransaction(transaction); syncGroup.markSyncReady(); - // Do this after maybeOnDragEnd(), because we use mReturnAnimator != null to - // imply + // Do this after maybeOnDragEnd(), because we use mReturnAnimator != null to imply // the drag was canceled rather than successful. mReturnAnimator = null; } @@ -649,14 +685,13 @@ public class TaskbarDragController extends DragController im if (tag instanceof ItemInfo) { ItemInfo item = (ItemInfo) tag; if (item.container == CONTAINER_ALL_APPS - || item.container == CONTAINER_PREDICTION + || item.container == CONTAINER_ALL_APPS_PREDICTION || isInSearchResultContainer(item)) { if (mDisallowGlobalDrag) { // We're dragging in taskbarAllApps, we don't have folders or shortcuts return iconView; } - // Since all apps closes when the drag starts, target the all apps button - // instead. + // Since all apps closes when the drag starts, target the all apps button instead. return taskbarViewController.getAllAppsButtonView(); } else if (item.container >= 0) { // Since folders close when the drag starts, target the folder icon instead. @@ -676,7 +711,8 @@ public class TaskbarDragController extends DragController im private static boolean isInSearchResultContainer(ItemInfo item) { ContainerInfo containerInfo = item.getContainerInfo(); return containerInfo.getContainerCase() == EXTENDED_CONTAINERS - && containerInfo.getExtendedContainers().getContainerCase() == DEVICE_SEARCH_RESULT_CONTAINER; + && containerInfo.getExtendedContainers().getContainerCase() + == DEVICE_SEARCH_RESULT_CONTAINER; } private void setupReturnDragAnimator(float fromX, float fromY, View originalView, @@ -705,7 +741,6 @@ public class TaskbarDragController extends DragController im final FloatProp mDy = new FloatProp(fromY, toPosition[1], FAST_OUT_SLOW_IN); final FloatProp mScale = new FloatProp(1f, toScale, FAST_OUT_SLOW_IN); final FloatProp mAlpha = new FloatProp(1f, toAlpha, Interpolators.ACCELERATE_2); - @Override public void onUpdate(float percent, boolean initOnly) { animListener.updateDragShadow(mDx.value, mDy.value, mScale.value, mAlpha.value); @@ -719,19 +754,15 @@ public class TaskbarDragController extends DragController im @Override protected float getX(MotionEvent ev) { - // We will resize to fill the screen while dragging, so use screen coordinates. - // This ensures - // we start at the correct position even though touch down is on the smaller - // DragLayer size. + // We will resize to fill the screen while dragging, so use screen coordinates. This ensures + // we start at the correct position even though touch down is on the smaller DragLayer size. return ev.getRawX(); } @Override protected float getY(MotionEvent ev) { - // We will resize to fill the screen while dragging, so use screen coordinates. - // This ensures - // we start at the correct position even though touch down is on the smaller - // DragLayer size. + // We will resize to fill the screen while dragging, so use screen coordinates. This ensures + // we start at the correct position even though touch down is on the smaller DragLayer size. return ev.getRawY(); } @@ -751,8 +782,11 @@ public class TaskbarDragController extends DragController im @Override public void addDropTarget(DropTarget target) { - // No-op as Taskbar currently doesn't support any drop targets internally. - // Note: if we do add internal DropTargets, we'll still need to ignore Folder. + if (target instanceof Folder) { + // we need to ignore Folder. + return; + } + super.addDropTarget(target); } @Override diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java index 046bb54419..5fedee6a8d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java @@ -18,7 +18,6 @@ package com.android.launcher3.taskbar; import static android.view.KeyEvent.ACTION_UP; import static android.view.KeyEvent.KEYCODE_BACK; -import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import android.content.Context; @@ -43,7 +42,6 @@ import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Utilities; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.views.BaseDragLayer; @@ -102,20 +100,16 @@ public class TaskbarDragLayer extends BaseDragLayer { public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, 1 /* alphaChannelCount */); - mBackgroundRenderer = new TaskbarBackgroundRenderer(mActivity); + mBackgroundRenderer = new TaskbarBackgroundRenderer(mContainer); mTaskbarBackgroundAlpha = new MultiPropertyFactory<>(this, BG_ALPHA, INDEX_COUNT, (a, b) -> a * b, 1f); mTaskbarBackgroundAlpha.get(INDEX_ALL_OTHER_STATES).setValue(0); - mTaskbarBackgroundAlpha.get(INDEX_STASH_ANIM).setValue( - enableScalingRevealHomeAnimation() && DisplayController.isTransientTaskbar(context) - ? 0 - : 1); } public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) { mControllerCallbacks = callbacks; - mBackgroundRenderer.updateStashedHandleWidth(mActivity, getResources()); + mBackgroundRenderer.updateStashedHandleWidth(mContainer, getResources()); recreateControllers(); } @@ -133,6 +127,7 @@ public class TaskbarDragLayer extends BaseDragLayer { @Override public void recreateControllers() { + super.recreateControllers(); mControllers = mControllerCallbacks.getTouchControllers(); } @@ -197,6 +192,7 @@ public class TaskbarDragLayer extends BaseDragLayer { @Override protected void dispatchDraw(Canvas canvas) { + if (mContainer.isDestroyed()) return; float backgroundHeight = mControllerCallbacks.getTaskbarBackgroundHeight() * (1f - mTaskbarBackgroundOffset); mBackgroundRenderer.setBackgroundHeight(backgroundHeight); @@ -265,6 +261,11 @@ public class TaskbarDragLayer extends BaseDragLayer { invalidate(); } + protected void setBackgroundTranslationXForBubbleBar(float translationX) { + mBackgroundRenderer.setTranslationXForBubbleBar(translationX); + invalidate(); + } + /** Returns the bounds in DragLayer coordinates of where the transient background was drawn. */ protected RectF getLastDrawnTransientRect() { return mBackgroundRenderer.getLastDrawnTransientRect(); @@ -273,6 +274,7 @@ public class TaskbarDragLayer extends BaseDragLayer { @Override public boolean dispatchTouchEvent(MotionEvent ev) { TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev); + mControllerCallbacks.onDispatchTouchEvent(ev); return super.dispatchTouchEvent(ev); } @@ -280,7 +282,7 @@ public class TaskbarDragLayer extends BaseDragLayer { @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) { - AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); + AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer); if (topView != null && topView.canHandleBack()) { topView.onBackInvoked(); // Handled by the floating view. @@ -290,6 +292,21 @@ public class TaskbarDragLayer extends BaseDragLayer { return super.dispatchKeyEvent(event); } + /** + * Sets animation boolean when only animating persistent taskbar. + */ + public void setIsAnimatingPersistentTaskbarBackground(boolean animatingPersistentTaskbarBg) { + mBackgroundRenderer.setAnimatingPersistentTaskbar(animatingPersistentTaskbarBg); + } + + /** + * Sets animation boolean when only animating transient taskbar. + */ + public void setIsAnimatingTransientTaskbarBackground(boolean animatingTransientTaskbarBg) { + mBackgroundRenderer.setAnimatingTransientTaskbar(animatingTransientTaskbarBg); + } + + /** * Sets the width percentage to inset the transient taskbar's background from the left and from * the right. diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java index ff890fb18e..8bd685b3d6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java @@ -15,21 +15,25 @@ */ package com.android.launcher3.taskbar; +import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.os.SystemProperties; +import android.view.MotionEvent; import android.view.ViewTreeObserver; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.util.DimensionUtils; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.TouchController; @@ -57,6 +61,8 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa private final AnimatedFloat mImeBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha); private final AnimatedFloat mAssistantBgTaskbar = new AnimatedFloat( this::updateBackgroundAlpha); + private final AnimatedFloat mBgTaskbarRecreate = new AnimatedFloat( + this::updateBackgroundAlpha); // Used to hide our background color when someone else (e.g. ScrimView) is handling it. private final AnimatedFloat mBgOverride = new AnimatedFloat(this::updateBackgroundAlpha); @@ -87,7 +93,10 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin); } - public void init(TaskbarControllers controllers) { + /** + * Init of taskbar drag layer controller + */ + public void init(TaskbarControllers controllers, AnimatorSet startAnimation) { mControllers = controllers; mTaskbarStashViaTouchController = new TaskbarStashViaTouchController(mControllers); mTaskbarDragLayer.init(new TaskbarDragLayerCallbacks()); @@ -95,15 +104,43 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa mOnBackgroundNavButtonColorIntensity = mControllers.navbarButtonsViewController .getOnTaskbarBackgroundNavButtonColorOverride(); - mTaskbarBackgroundProgress.updateValue(DisplayController.isTransientTaskbar(mActivity) - ? PINNING_TRANSIENT - : PINNING_PERSISTENT); + + if (startAnimation != null) { + // set taskbar background render animation boolean + if (mActivity.isTransientTaskbar()) { + mTaskbarDragLayer.setIsAnimatingTransientTaskbarBackground(true); + } else { + mTaskbarDragLayer.setIsAnimatingPersistentTaskbarBackground(true); + } + + float desiredValue = mActivity.isTransientTaskbar() + ? PINNING_TRANSIENT + : PINNING_PERSISTENT; + + float nonDesiredvalue = + !mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT; + + ObjectAnimator objectAnimator = mTaskbarBackgroundProgress.animateToValue( + nonDesiredvalue, desiredValue); + objectAnimator.setInterpolator(EMPHASIZED); + startAnimation.play(objectAnimator); + startAnimation.addListener(AnimatorListeners.forEndCallback(()-> { + // reset taskbar background render animation boolean + mTaskbarDragLayer.setIsAnimatingPersistentTaskbarBackground(false); + mTaskbarDragLayer.setIsAnimatingTransientTaskbarBackground(false); + })); + + } else { + mTaskbarBackgroundProgress.updateValue( + mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT); + } mBgTaskbar.value = 1; mKeyguardBgTaskbar.value = 1; mNotificationShadeBgTaskbar.value = 1; mImeBgTaskbar.value = 1; mAssistantBgTaskbar.value = 1; + mBgTaskbarRecreate.value = 1; mBgOverride.value = 1; updateBackgroundAlpha(); @@ -111,6 +148,13 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa updateTaskbarAlpha(); } + /** + * Called when destroying Taskbar with animation. + */ + public void onDestroyAnimation(AnimatorSet animatorSet) { + animatorSet.play(mBgTaskbarRecreate.animateToValue(0f)); + } + public void onDestroy() { mTaskbarDragLayer.onDestroy(); } @@ -119,9 +163,14 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa * @return Bounds (in TaskbarDragLayer coordinates) where an opened Folder can display. */ public Rect getFolderBoundingBox() { - Rect boundingBox = new Rect(0, 0, mTaskbarDragLayer.getWidth(), - mTaskbarDragLayer.getHeight() - mActivity.getDeviceProfile().taskbarHeight - - mActivity.getDeviceProfile().taskbarBottomMargin); + Rect boundingBox = new Rect( + 0, + 0, + mTaskbarDragLayer.getWidth(), + mTaskbarDragLayer.getHeight() + - mActivity.getDeviceProfile().getTaskbarProfile().getHeight() + - mActivity.getDeviceProfile().getTaskbarProfile().getBottomMargin() + ); boundingBox.inset(mFolderMargin, mFolderMargin); return boundingBox; } @@ -171,10 +220,14 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa } private void updateBackgroundAlpha() { + if (!mActivity.drawsTaskbarBackground() || mActivity.isDestroyed()) { + return; + } + final float bgNavbar = mBgNavbar.value; final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value * mNotificationShadeBgTaskbar.value * mImeBgTaskbar.value - * mAssistantBgTaskbar.value; + * mAssistantBgTaskbar.value * mBgTaskbarRecreate.value; mLastSetBackgroundAlpha = mBgOverride.value * Math.max(bgNavbar, bgTaskbar); mBackgroundRendererAlpha.setValue(mLastSetBackgroundAlpha); @@ -192,6 +245,13 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa mTaskbarDragLayer.setBackgroundTranslationYForSwipe(transY); } + /** + * Sets the translation of the background for the bubble bar. + */ + public void setTranslationXForBubbleBar(float transX) { + mTaskbarDragLayer.setBackgroundTranslationXForBubbleBar(transX); + } + /** * Sets the translation of the background during the spring on stash animation. */ @@ -254,6 +314,7 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa pw.println(prefix + "\t\tmNotificationShadeBgTaskbar=" + mNotificationShadeBgTaskbar.value); pw.println(prefix + "\t\tmImeBgTaskbar=" + mImeBgTaskbar.value); pw.println(prefix + "\t\tmAssistantBgTaskbar=" + mAssistantBgTaskbar.value); + pw.println(prefix + "\t\tmBgTaskbarRecreate=" + mBgTaskbarRecreate.value); } /** @@ -291,12 +352,12 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa if (mActivity.isPhoneMode()) { Resources resources = mActivity.getResources(); Point taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, - resources, true /* isPhoneMode */); + resources, true /* isPhoneMode */, mActivity.isGestureNav()); return taskbarDimensions.y == -1 ? deviceProfile.getDisplayInfo().currentSize.y : taskbarDimensions.y; } else { - return deviceProfile.taskbarHeight; + return deviceProfile.getTaskbarProfile().getHeight(); } } @@ -321,5 +382,15 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa } mControllers.taskbarInsetsController.drawDebugTouchableRegionBounds(canvas); } + + /** Handles any touch event before it is dispatched to the rest of TaskbarDragLayer. */ + public void onDispatchTouchEvent(MotionEvent ev) { + if (mActivity.isThreeButtonNav() && ev.getAction() == MotionEvent.ACTION_OUTSIDE + && mControllers.uiController.isAnimatingToHotseat()) { + // When touching during animation to home, jump to the end so Hotseat can handle + // the touch. (Gesture Navigation handles this in AbsSwipeUpHandler.) + mControllers.uiController.endAnimationToHotseat(); + } + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt index 5adb25821d..eb2d1d98d1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt @@ -29,7 +29,6 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.animation.Interpolator import android.window.OnBackInvokedDispatcher import androidx.core.view.updateLayoutParams -import app.lawnchair.theme.color.tokens.ColorTokens import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.app.animation.Interpolators.STANDARD @@ -40,22 +39,20 @@ import com.android.launcher3.popup.RoundedArrowDrawable import com.android.launcher3.util.Themes import com.android.launcher3.views.ActivityContext +import app.lawnchair.theme.color.tokens.ColorTokens + private const val ENTER_DURATION_MS = 300L private const val EXIT_DURATION_MS = 150L /** Floating tooltip for Taskbar education. */ class TaskbarEduTooltip @JvmOverloads -constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, -) : AbstractFloatingView(context, attrs, defStyleAttr) { +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + AbstractFloatingView(context, attrs, defStyleAttr) { private val activityContext: ActivityContext = ActivityContext.lookupContext(context) - private val backgroundColor = - ColorTokens.SurfaceBrightColor.resolveColor(getContext()) + private val backgroundColor = ColorTokens.SurfaceBrightColor.resolveColor(getContext()) private val tooltipCornerRadius = Themes.getDialogCornerRadius(context) private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width) @@ -90,8 +87,8 @@ constructor( // constraint to reduce the number of lines of text and hopefully free up some height. activityContext.dragLayer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) if ( - measuredHeight + activityContext.deviceProfile.taskbarHeight >= - activityContext.deviceProfile.availableHeightPx + measuredHeight + activityContext.deviceProfile.taskbarProfile.height >= + activityContext.deviceProfile.deviceProperties.availableHeightPx ) { updateLayoutParams { width = MATCH_PARENT } } @@ -102,8 +99,8 @@ constructor( override fun onFinishInflate() { super.onFinishInflate() - content = requireViewById(R.id.content) - arrow = requireViewById(R.id.arrow) + content = requireViewById(R.id.content) + arrow = requireViewById(R.id.arrow) arrow.background = RoundedArrowDrawable( arrowWidth, @@ -151,6 +148,7 @@ constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() + // Lawnchair-TODO-Merge: This was disabled but enabled in 16r2, this function likely disable the gesture system during edu // findOnBackInvokedDispatcher() // ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this) } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt index d57c4838d7..9a120f5611 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt @@ -33,22 +33,26 @@ import android.view.accessibility.AccessibilityNodeInfo import android.widget.TextView import androidx.annotation.IntDef import androidx.annotation.LayoutRes +import androidx.annotation.VisibleForTesting import androidx.core.text.HtmlCompat import androidx.core.view.updateLayoutParams import com.airbnb.lottie.LottieAnimationView import com.android.launcher3.LauncherPrefs import com.android.launcher3.R +import com.android.launcher3.RemoveAnimationSettingsTracker import com.android.launcher3.Utilities import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController -import com.android.launcher3.util.DisplayController import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN import com.android.launcher3.util.ResourceBasedOverride import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.BaseDragLayer +import com.android.quickstep.util.ContextualSearchInvoker import com.android.quickstep.util.LottieAnimationColorUtils +import com.android.wm.shell.shared.TypefaceUtils +import com.android.wm.shell.shared.TypefaceUtils.FontFamily import java.io.PrintWriter /** First EDU step for swiping up to show transient Taskbar. */ @@ -79,7 +83,11 @@ open class TaskbarEduTooltipController(context: Context) : ResourceBasedOverride, LoggableTaskbarController { protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) - open val shouldShowSearchEdu = false + open val shouldShowSearchEdu: Boolean + get() = + ContextualSearchInvoker(activityContext) + .runContextualSearchInvocationChecksAndLogFailures() + private val isTooltipEnabled: Boolean get() { return !Utilities.isRunningInTestHarness() && @@ -87,7 +95,7 @@ open class TaskbarEduTooltipController(context: Context) : !activityContext.isTinyTaskbar } - private val isOpen: Boolean + val isTooltipOpen: Boolean get() = tooltip?.isOpen ?: false val isBeforeTooltipFeaturesStep: Boolean @@ -96,7 +104,8 @@ open class TaskbarEduTooltipController(context: Context) : private lateinit var controllers: TaskbarControllers // Keep track of whether the user has seen the Search Edu - private var userHasSeenSearchEdu: Boolean + @VisibleForTesting + var userHasSeenSearchEdu: Boolean get() { return TASKBAR_SEARCH_EDU_SEEN.get(activityContext) } @@ -121,11 +130,31 @@ open class TaskbarEduTooltipController(context: Context) : activityContext.dragLayer.post { maybeShowSearchEdu() } } + /** + * Turns off auto play of lottie animations if user has opted to remove animation else attaches + * click listener to allow user to play or pause animations. + */ + fun handleEduAnimations(animationViews: List) { + for (animationView in animationViews) { + if ( + RemoveAnimationSettingsTracker.INSTANCE.get(animationView.context) + .isRemoveAnimationEnabled() + ) { + animationView.pauseAnimation() + } else { + animationView.setOnClickListener { + if (animationView.isAnimating) animationView.pauseAnimation() + else animationView.playAnimation() + } + } + } + } + /** Shows swipe EDU tooltip if it is the current [tooltipStep]. */ fun maybeShowSwipeEdu() { if ( !isTooltipEnabled || - !DisplayController.isTransientTaskbar(activityContext) || + !activityContext.isTransientTaskbar || tooltipStep > TOOLTIP_STEP_SWIPE ) { return @@ -134,7 +163,15 @@ open class TaskbarEduTooltipController(context: Context) : tooltipStep = TOOLTIP_STEP_FEATURES inflateTooltip(R.layout.taskbar_edu_swipe) tooltip?.run { - requireViewById(R.id.swipe_animation).supportLightTheme() + TypefaceUtils.setTypeface( + requireViewById(R.id.taskbar_edu_title), + FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED, + ) + val swipeAnimation = requireViewById(R.id.swipe_animation) + swipeAnimation.contentDescription = + context.getString(R.string.taskbar_edu_swipe_animation_description) + swipeAnimation.supportLightTheme() + handleEduAnimations(listOf(swipeAnimation)) show() } } @@ -157,13 +194,20 @@ open class TaskbarEduTooltipController(context: Context) : tooltip?.run { allowTouchDismissal = false val splitscreenAnim = requireViewById(R.id.splitscreen_animation) + splitscreenAnim.contentDescription = + context.getString(R.string.taskbar_edu_split_screen_animation_description) val suggestionsAnim = requireViewById(R.id.suggestions_animation) + suggestionsAnim.contentDescription = + context.getString(R.string.taskbar_edu_suggested_app_animation_description) val pinningAnim = requireViewById(R.id.pinning_animation) + pinningAnim.contentDescription = + context.getString(R.string.taskbar_edu_pinning_animation_description) val pinningEdu = requireViewById(R.id.pinning_edu) splitscreenAnim.supportLightTheme() suggestionsAnim.supportLightTheme() pinningAnim.supportLightTheme() - if (DisplayController.isTransientTaskbar(activityContext)) { + handleEduAnimations(listOf(splitscreenAnim, suggestionsAnim, pinningAnim)) + if (activityContext.isTransientTaskbar) { splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient) suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient) pinningEdu.visibility = if (enableTaskbarPinning()) VISIBLE else GONE @@ -173,10 +217,27 @@ open class TaskbarEduTooltipController(context: Context) : pinningEdu.visibility = GONE } + TypefaceUtils.setTypeface( + requireViewById(R.id.taskbar_edu_title), + FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED, + ) + TypefaceUtils.setTypeface( + requireViewById(R.id.splitscreen_text), + FontFamily.GSF_BODY_MEDIUM, + ) + TypefaceUtils.setTypeface( + requireViewById(R.id.suggestions_text), + FontFamily.GSF_BODY_MEDIUM, + ) + TypefaceUtils.setTypeface( + requireViewById(R.id.pinning_text), + FontFamily.GSF_BODY_MEDIUM, + ) + // Set up layout parameters. content.updateLayoutParams { width = MATCH_PARENT } updateLayoutParams { - if (DisplayController.isTransientTaskbar(activityContext)) { + if (activityContext.isTransientTaskbar) { width = resources.getDimensionPixelSize( if (enableTaskbarPinning()) @@ -184,7 +245,7 @@ open class TaskbarEduTooltipController(context: Context) : else R.dimen.taskbar_edu_features_tooltip_width_with_two_features ) - bottomMargin += activityContext.deviceProfile.taskbarHeight + bottomMargin += activityContext.deviceProfile.taskbarProfile.height } else { width = resources.getDimensionPixelSize( @@ -209,7 +270,7 @@ open class TaskbarEduTooltipController(context: Context) : // for the original 2 edu steps) as a proxy to needing to show the separate pinning edu if ( !enableTaskbarPinning() || - !DisplayController.isTransientTaskbar(activityContext) || + !activityContext.isTransientTaskbar || !isTooltipEnabled || tooltipStep > TOOLTIP_STEP_PINNING || tooltipStep < TOOLTIP_STEP_FEATURES @@ -221,12 +282,24 @@ open class TaskbarEduTooltipController(context: Context) : tooltip?.run { allowTouchDismissal = true - requireViewById(R.id.standalone_pinning_animation) - .supportLightTheme() + TypefaceUtils.setTypeface( + requireViewById(R.id.taskbar_edu_title), + FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED, + ) + TypefaceUtils.setTypeface( + requireViewById(R.id.pinning_text), + FontFamily.GSF_BODY_MEDIUM, + ) + val pinningAnim = + requireViewById(R.id.standalone_pinning_animation) + pinningAnim.contentDescription = + context.getString(R.string.taskbar_edu_pinning_animation_description) + pinningAnim.supportLightTheme() + handleEduAnimations(listOf(pinningAnim)) updateLayoutParams { - if (DisplayController.isTransientTaskbar(activityContext)) { - bottomMargin += activityContext.deviceProfile.taskbarHeight + if (activityContext.isTransientTaskbar) { + bottomMargin += activityContext.deviceProfile.taskbarProfile.height } // Unlike other tooltips, we want to align with taskbar divider rather than center. gravity = Gravity.BOTTOM @@ -255,10 +328,11 @@ open class TaskbarEduTooltipController(context: Context) : fun maybeShowSearchEdu() { if ( !enableTaskbarPinning() || - !DisplayController.isPinnedTaskbar(activityContext) || + !activityContext.isPinnedTaskbar || !isTooltipEnabled || !shouldShowSearchEdu || - userHasSeenSearchEdu + userHasSeenSearchEdu || + !controllers.taskbarStashController.isTaskbarVisibleAndNotStashing ) { return } @@ -266,12 +340,23 @@ open class TaskbarEduTooltipController(context: Context) : inflateTooltip(R.layout.taskbar_edu_search) tooltip?.run { allowTouchDismissal = true - requireViewById(R.id.search_edu_animation).supportLightTheme() + val searchEdu = requireViewById(R.id.search_edu_animation) + searchEdu.contentDescription = + context.getString(R.string.taskbar_edu_suggested_search_animation_description) + searchEdu.supportLightTheme() + handleEduAnimations(listOf(searchEdu)) val eduSubtitle: TextView = requireViewById(R.id.search_edu_text) + + TypefaceUtils.setTypeface( + requireViewById(R.id.taskbar_edu_title), + FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED, + ) + TypefaceUtils.setTypeface(eduSubtitle, FontFamily.GSF_BODY_SMALL) + showDisclosureText(eduSubtitle) updateLayoutParams { - if (DisplayController.isTransientTaskbar(activityContext)) { - bottomMargin += activityContext.deviceProfile.taskbarHeight + if (activityContext.isTransientTaskbar) { + bottomMargin += activityContext.deviceProfile.taskbarProfile.height } // Unlike other tooltips, we want to align with the all apps button rather than // center. @@ -349,19 +434,19 @@ open class TaskbarEduTooltipController(context: Context) : overlayContext.layoutInflater.inflate( R.layout.taskbar_edu_tooltip, overlayContext.dragLayer, - false + false, ) as TaskbarEduTooltip controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, - true + true, ) tooltip.onCloseCallback = { this.tooltip = null controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, - false + false, ) controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) } @@ -376,7 +461,7 @@ open class TaskbarEduTooltipController(context: Context) : override fun performAccessibilityAction( host: View, action: Int, - args: Bundle? + args: Bundle?, ): Boolean { if (action == R.id.close) { hide() @@ -394,13 +479,13 @@ open class TaskbarEduTooltipController(context: Context) : override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfo + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) info.addAction( AccessibilityNodeInfo.AccessibilityAction( R.id.close, - host.context?.getText(R.string.taskbar_edu_close) + host.context?.getText(R.string.taskbar_edu_close), ) ) } @@ -409,7 +494,7 @@ open class TaskbarEduTooltipController(context: Context) : override fun dumpLogs(prefix: String?, pw: PrintWriter?) { pw?.println(prefix + "TaskbarEduTooltipController:") pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled") - pw?.println("$prefix\tisOpen=$isOpen") + pw?.println("$prefix\tisOpen=$isTooltipOpen") pw?.println("$prefix\ttooltipStep=$tooltipStep") } @@ -419,7 +504,7 @@ open class TaskbarEduTooltipController(context: Context) : return ResourceBasedOverride.Overrides.getObject( TaskbarEduTooltipController::class.java, context, - R.string.taskbar_edu_tooltip_controller_class + R.string.taskbar_edu_tooltip_controller_class, ) } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java index 6ac862e9e0..b7f55756c1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java @@ -85,6 +85,9 @@ public class TaskbarForceVisibleImmersiveController implements TouchController { /** Update values tracked via sysui flags. */ public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) { + if (mContext.isPhoneMode()) { + return; + } mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0; if (mContext.isNavBarForceVisible()) { if (mIsImmersiveMode) { @@ -105,8 +108,10 @@ public class TaskbarForceVisibleImmersiveController implements TouchController { /** Clean up animations. */ public void onDestroy() { startIconUndimming(); - mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null); - mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null); + if (mControllers != null) { + mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null); + mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null); + } } private void startIconUndimming() { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java index 044319796e..42ad5b2aa1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java @@ -18,29 +18,24 @@ package com.android.launcher3.taskbar; import static android.view.MotionEvent.ACTION_HOVER_ENTER; import static android.view.MotionEvent.ACTION_HOVER_EXIT; import static android.view.View.ALPHA; -import static android.view.View.SCALE_Y; -import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT; -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_EXCEPT_ON_BOARD_POPUP; +import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP; +import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER; import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS; -import static com.android.launcher3.views.ArrowTipView.TEXT_ALPHA; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; +import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.MotionEvent; import android.view.View; -import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.compat.AccessibilityManagerCompat; +import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.views.ArrowTipView; @@ -48,19 +43,15 @@ import com.android.launcher3.views.ArrowTipView; * Controls showing a tooltip in the taskbar above each icon when it is hovered. */ public class TaskbarHoverToolTipController implements View.OnHoverListener { - - private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 250; - private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150; - - private final Handler mHoverToolTipHandler = new Handler(Looper.getMainLooper()); - private final Runnable mRevealHoverToolTipRunnable = this::revealHoverToolTip; - private final Runnable mHideHoverToolTipRunnable = this::hideHoverToolTip; + // Short duration to reveal tooltip, as it is positioned in the x/y via a post() call in + // parallel with the open animation. An instant animation could show in the wrong location. + private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 15; private final TaskbarActivityContext mActivity; private final TaskbarView mTaskbarView; private final View mHoverView; private final ArrowTipView mHoverToolTipView; - private final String mToolTipText; + private final int mYOffset; public TaskbarHoverToolTipController(TaskbarActivityContext activity, TaskbarView taskbarView, View hoverView) { @@ -68,15 +59,6 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener { mTaskbarView = taskbarView; mHoverView = hoverView; - if (mHoverView instanceof BubbleTextView) { - mToolTipText = ((BubbleTextView) mHoverView).getText().toString(); - } else if (mHoverView instanceof FolderIcon - && ((FolderIcon) mHoverView).mInfo.title != null) { - mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString(); - } else { - mToolTipText = null; - } - ContextThemeWrapper arrowContextWrapper = new ContextThemeWrapper(mActivity, R.style.ArrowTipTaskbarStyle); mHoverToolTipView = new ArrowTipView(arrowContextWrapper, /* isPointingUp = */ false, @@ -87,90 +69,75 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener { R.dimen.taskbar_tooltip_horizontal_padding); mHoverToolTipView.findViewById(R.id.text).setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); - - AnimatorSet hoverCloseAnimator = new AnimatorSet(); - ObjectAnimator textCloseAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0); - textCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0, 0.33f)); - ObjectAnimator alphaCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0); - alphaCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 0.66f)); - ObjectAnimator scaleCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0); - scaleCloseAnimator.setInterpolator(Interpolators.STANDARD); - hoverCloseAnimator.playTogether( - textCloseAnimator, - alphaCloseAnimator, - scaleCloseAnimator); - hoverCloseAnimator.setStartDelay(0); - hoverCloseAnimator.setDuration(HOVER_TOOL_TIP_EXIT_DURATION); - mHoverToolTipView.setCustomCloseAnimation(hoverCloseAnimator); + mHoverToolTipView.setAlpha(0); + mYOffset = arrowContextWrapper.getResources().getDimensionPixelSize( + R.dimen.taskbar_tooltip_y_offset); AnimatorSet hoverOpenAnimator = new AnimatorSet(); - ObjectAnimator textOpenAnimator = - ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0, 255); - textOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.15f, 0.75f)); - ObjectAnimator scaleOpenAnimator = - ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0f, 1f); - scaleOpenAnimator.setInterpolator(Interpolators.EMPHASIZED); ObjectAnimator alphaOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0f, 1f); - alphaOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0f, 0.33f)); - hoverOpenAnimator.playTogether( - scaleOpenAnimator, - textOpenAnimator, - alphaOpenAnimator); + hoverOpenAnimator.play(alphaOpenAnimator); hoverOpenAnimator.setDuration(HOVER_TOOL_TIP_REVEAL_DURATION); mHoverToolTipView.setCustomOpenAnimation(hoverOpenAnimator); mHoverToolTipView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { mHoverToolTipView.setPivotY(bottom); - mHoverToolTipView.setY(mTaskbarView.getTop() - (bottom - top)); + mHoverToolTipView.setY(mTaskbarView.getTop() - mYOffset - (bottom - top)); }); } @Override public boolean onHover(View v, MotionEvent event) { - boolean isAnyOtherFloatingViewOpen = - AbstractFloatingView.hasOpenView(mActivity, TYPE_ALL_EXCEPT_ON_BOARD_POPUP); - if (isAnyOtherFloatingViewOpen) { - mHoverToolTipHandler.removeCallbacksAndMessages(null); - } // If hover leaves a taskbar icon animate the tooltip closed. if (event.getAction() == ACTION_HOVER_EXIT) { - startHideHoverToolTip(); + mHoverToolTipView.close(/* animate= */ false); mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false); - return true; - } else if (!isAnyOtherFloatingViewOpen && event.getAction() == ACTION_HOVER_ENTER) { - // If hovering above a taskbar icon starts, animate the tooltip open. Do not - // reveal if any floating views such as folders or edu pop-ups are open. - startRevealHoverToolTip(); + } else if (event.getAction() == ACTION_HOVER_ENTER) { + maybeRevealHoverToolTip(); mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true); - return true; } return false; } - private void startRevealHoverToolTip() { - mHoverToolTipHandler.post(mRevealHoverToolTipRunnable); - } + private void maybeRevealHoverToolTip() { + if (mHoverView == null) { + return; + } - private void revealHoverToolTip() { - if (mHoverView == null || mToolTipText == null) { + final String toolTipText = getToolTipText(); + if (TextUtils.isEmpty(toolTipText)) { + return; + } + + // Do not show tooltip if taskbar icons are transitioning to hotseat. + if (mActivity.isIconAlignedWithHotseat()) { return; } if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) { return; } + // Do not reveal if floating views such as folders or app pop-ups are open, + // as these views will overlap and not look great. + if (AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER | TYPE_ACTION_POPUP)) { + return; + } + Rect iconViewBounds = Utilities.getViewBounds(mHoverView); - mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(), - mTaskbarView.getTop(), /* shouldAutoClose= */ false); + mHoverToolTipView.showAtLocation(toolTipText, iconViewBounds.centerX(), + mTaskbarView.getTop() - mYOffset, /* shouldAutoClose= */ false); } - private void startHideHoverToolTip() { - int accessibilityHideTimeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis( - mActivity, /* originalTimeout= */ 0, FLAG_CONTENT_TEXT); - mHoverToolTipHandler.postDelayed(mHideHoverToolTipRunnable, accessibilityHideTimeout); - } - - private void hideHoverToolTip() { - mHoverToolTipView.close(/* animate = */ true); + private String getToolTipText() { + if (mHoverView instanceof BubbleTextView btv) { + return btv.getText().toString(); + } else if (mHoverView instanceof FolderIcon icon && icon.mInfo.title != null) { + return icon.mInfo.title.toString(); + } else if (mHoverView instanceof AppPairIcon icon) { + return icon.getTitleTextView().getText().toString(); + } else if (mHoverView instanceof TaskbarOverflowView icon) { + return icon.getTextForTooltipPopup(); + } else { + return null; + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt index 01eeb082d5..21fe03f290 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt @@ -21,7 +21,6 @@ import android.graphics.Insets import android.graphics.Paint import android.graphics.Rect import android.graphics.Region -import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR import android.os.Binder import android.os.IBinder import android.view.DisplayInfo @@ -33,8 +32,10 @@ import android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER import android.view.InsetsSource.FLAG_SUPPRESS_SCRIM import android.view.Surface import android.view.ViewTreeObserver +import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION +import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE import android.view.WindowInsets import android.view.WindowInsets.Type.mandatorySystemGestures import android.view.WindowInsets.Type.navigationBars @@ -43,7 +44,9 @@ import android.view.WindowInsets.Type.tappableElement import android.view.WindowManager import android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD import android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION +import androidx.annotation.VisibleForTesting import androidx.core.graphics.toRegion +import com.android.app.tracing.traceSection import com.android.internal.policy.GestureNavigationSettingsObserver import com.android.launcher3.DeviceProfile import com.android.launcher3.R @@ -51,12 +54,18 @@ import com.android.launcher3.anim.AlphaUpdateListener import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION import com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.DEFAULT_TOUCH_REGION +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.DRAG_LAYER_INVISIBLE +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.FULLSCREEN_TASKBAR_WINDOW +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.ICONS_INVISIBLE +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.PHONE_MODE +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.SYSTEM_DRAG_IN_PROGRESS +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.TRANSIENT_IN_OVERVIEW +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.UI_CONTROLLER_UNTOUCHABLE import com.android.launcher3.testing.shared.ResourceUtils -import com.android.launcher3.util.DisplayController import com.android.launcher3.util.Executors import java.io.PrintWriter import kotlin.jvm.optionals.getOrNull -import kotlin.math.max /** Handles the insets that Taskbar provides to underlying apps and the IME. */ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController { @@ -64,7 +73,11 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas companion object { private const val INDEX_LEFT = 0 private const val INDEX_RIGHT = 1 - const val FLAG_INSETS_ROUNDED_CORNER = 1 shl 1 + private const val TAG = "TaskbarInsetsController" + + private fun Region.addBoundsToRegion(bounds: Rect?) { + bounds?.let { op(it, Region.Op.UNION) } + } } /** The bottom insets taskbar provides to the IME when IME is visible. */ @@ -80,9 +93,9 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas context.mainThreadHandler, Executors.UI_HELPER_EXECUTOR.handler, context, - this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged + this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged, ) - private val debugTouchableRegion = DebugTouchableRegion() + @VisibleForTesting val debugTouchableRegion = DebugTouchableRegion() // Initialized in init. private lateinit var controllers: TaskbarControllers @@ -94,11 +107,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas onTaskbarOrBubblebarWindowHeightOrInsetsChanged() context.addOnDeviceProfileChangeListener(deviceProfileChangeListener) - try { - gestureNavSettingsObserver.registerForCallingUser() - } catch (t: Throwable) { - // Ignore - } + gestureNavSettingsObserver.registerForCallingUser() } fun onDestroy() { @@ -106,91 +115,83 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas gestureNavSettingsObserver.unregister() } - fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() { - val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps - // We only report tappableElement height for unstashed, persistent taskbar, - // which is also when we draw the rounded corners above taskbar. - val insetsRoundedCornerFlag = - if (tappableHeight > 0) { - FLAG_INSETS_ROUNDED_CORNER - } else { - 0 + fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() = + traceSection("$TAG.onTaskbarOrBubblebarWindowHeightOrInsetsChanged") { + val taskbarStashController = controllers.taskbarStashController + val tappableHeight = taskbarStashController.tappableHeightToReportToApps + // We only report tappableElement height for unstashed, persistent taskbar, + // which is also when we draw the rounded corners above taskbar on tablets. + val insetsRoundedCornerFlag = + if (tappableHeight > 0 && context.drawsTaskbarBackground()) { + FLAG_INSETS_ROUNDED_CORNER + } else { + 0 + } + + windowLayoutParams.providedInsets = + if (enableTaskbarNoRecreate() && controllers.sharedState != null) { + getProvidedInsets( + controllers.sharedState!!.insetsFrameProviders, + insetsRoundedCornerFlag, + ) + } else { + getProvidedInsets(insetsRoundedCornerFlag) + } + + if (windowLayoutParams.paramsForRotation != null) { + for (layoutParams in windowLayoutParams.paramsForRotation) { + layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag) + } } - windowLayoutParams.providedInsets = - if (enableTaskbarNoRecreate() && controllers.sharedState != null) { - getProvidedInsets( - controllers.sharedState!!.insetsFrameProviders, - insetsRoundedCornerFlag - ) - } else { - getProvidedInsets(insetsRoundedCornerFlag) + val bubbleControllers = controllers.bubbleControllers.getOrNull() + val taskbarTouchableHeight = taskbarStashController.touchableHeight + val bubblesTouchableHeight = + bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0 + // reset touch bounds + defaultTouchableRegion.setEmpty() + if (bubbleControllers != null) { + val bubbleBarViewController = bubbleControllers.bubbleBarViewController + val isBubbleBarVisible = + bubbleControllers.bubbleStashController.isBubbleBarVisible() + val isAnimatingNewBubble = bubbleBarViewController.isAnimatingNewBubble + // if bubble bar is visible or animating new bubble, add bar bounds to the touch + // region + if (isBubbleBarVisible || isAnimatingNewBubble) { + defaultTouchableRegion.addBoundsToRegion( + bubbleBarViewController.bubbleBarBounds + ) + defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.flyoutBounds) + } + } + if ( + taskbarStashController.isInApp || + controllers.uiController.isInOverviewUi || + context.showLockedTaskbarOnHome() + ) { + // only add the taskbar touch region if not on home + val bottom = windowLayoutParams.height + val top = bottom - taskbarTouchableHeight + val right = context.deviceProfile.deviceProperties.widthPx + defaultTouchableRegion.addBoundsToRegion(Rect(/* left= */ 0, top, right, bottom)) } - if (windowLayoutParams.paramsForRotation != null) { - for (layoutParams in windowLayoutParams.paramsForRotation) { - layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag) + // Pre-calculate insets for different providers across different rotations for this + // gravity + for (rotation in Surface.ROTATION_0..Surface.ROTATION_270) { + // Add insets for navbar rotated params + val layoutParams = windowLayoutParams.paramsForRotation[rotation] + for (provider in layoutParams.providedInsets) { + setProviderInsets(provider, layoutParams.gravity, rotation) + } } + // Also set the parent providers (i.e. not in paramsForRotation). + for (provider in windowLayoutParams.providedInsets) { + setProviderInsets(provider, windowLayoutParams.gravity, context.display.rotation) + } + context.notifyUpdateLayoutParams() } - val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight - val bubblesTouchableHeight = - if (controllers.bubbleControllers.isPresent) { - controllers.bubbleControllers.get().bubbleStashController.touchableHeight - } else { - 0 - } - val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight) - - if ( - controllers.bubbleControllers.isPresent && - controllers.bubbleControllers.get().bubbleStashController.isBubblesShowingOnHome - ) { - val iconBounds = - controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds - defaultTouchableRegion.set( - iconBounds.left, - iconBounds.top, - iconBounds.right, - iconBounds.bottom - ) - } else { - defaultTouchableRegion.set( - 0, - windowLayoutParams.height - touchableHeight, - context.deviceProfile.widthPx, - windowLayoutParams.height - ) - - // if there's an animating bubble add it to the touch region so that it's clickable - val isAnimatingNewBubble = - controllers.bubbleControllers - .getOrNull() - ?.bubbleBarViewController - ?.isAnimatingNewBubble - ?: false - if (isAnimatingNewBubble) { - val iconBounds = - controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds - defaultTouchableRegion.op(iconBounds, Region.Op.UNION) - } - } - - // Pre-calculate insets for different providers across different rotations for this gravity - for (rotation in Surface.ROTATION_0..Surface.ROTATION_270) { - // Add insets for navbar rotated params - val layoutParams = windowLayoutParams.paramsForRotation[rotation] - for (provider in layoutParams.providedInsets) { - setProviderInsets(provider, layoutParams.gravity, rotation) - } - } - // Also set the parent providers (i.e. not in paramsForRotation). - for (provider in windowLayoutParams.providedInsets) { - setProviderInsets(provider, windowLayoutParams.gravity, context.display.rotation) - } - context.notifyUpdateLayoutParams() - } - /** * This is for when ENABLE_TASKBAR_NO_RECREATION is enabled. We generate one instance of * providedInsets and use it across the entire lifecycle of TaskbarManager. The only thing we @@ -198,7 +199,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas */ private fun getProvidedInsets( providedInsets: Array, - insetsRoundedCornerFlag: Int + insetsRoundedCornerFlag: Int, ): Array { val navBarsFlag = (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag @@ -224,14 +225,14 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas InsetsFrameProvider(insetsOwner, 0, navigationBars()) .setFlags( navBarsFlag, - FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING or FLAG_INSETS_ROUNDED_CORNER + FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING or FLAG_INSETS_ROUNDED_CORNER, ), InsetsFrameProvider(insetsOwner, 0, tappableElement()), InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()), InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures()) .setSource(SOURCE_DISPLAY), InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures()) - .setSource(SOURCE_DISPLAY) + .setSource(SOURCE_DISPLAY), ) } @@ -243,20 +244,19 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, endRotation) } else if (provider.type == mandatorySystemGestures()) { if (context.isThreeButtonNav) { - provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, - endRotation) + provider.insetsSize = + getInsetsForGravityWithCutout(contentHeight, gravity, endRotation) } else { val gestureHeight = - ResourceUtils.getNavbarSize( + ResourceUtils.getNavbarSize( ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, - context.resources) - val isPinnedTaskbar = context.deviceProfile.isTaskbarPresent - && !context.deviceProfile.isTransientTaskbar - val mandatoryGestureHeight = - if (isPinnedTaskbar) contentHeight - else gestureHeight - provider.insetsSize = getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, - endRotation) + context.resources, + ) + val isPinnedTaskbar = + context.deviceProfile.isTaskbarPresent && !context.isTransientTaskbar + val mandatoryGestureHeight = if (isPinnedTaskbar) contentHeight else gestureHeight + provider.insetsSize = + getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, endRotation) } } else if (provider.type == tappableElement()) { provider.insetsSize = getInsetsForGravity(tappableHeight, gravity) @@ -275,7 +275,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas // When in gesture nav, report the stashed height to the IME, to allow hiding the // IME navigation bar. val imeInsetsSize = - if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) { + if (context.isGestureNav) { getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity) } else { getInsetsForGravity(taskbarHeightForIme, gravity) @@ -289,8 +289,8 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas // override below (insetsSizeOverrides must have the same length and // types after the window is added according to // WindowManagerService#relayoutWindow) - provider.insetsSize - ) + provider.insetsSize, + ), ) // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled. val visInsetsSizeForTappableElement = @@ -301,7 +301,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize), InsetsFrameProvider.InsetsSizeOverride( TYPE_VOICE_INTERACTION, - visInsetsSizeForTappableElement + visInsetsSizeForTappableElement, ), ) if ( @@ -363,48 +363,35 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas */ fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) { insetsInfo.touchableRegion.setEmpty() - // Always have nav buttons be touchable - controllers.navbarButtonsViewController.addVisibleButtonsRegion( - context.dragLayer, - insetsInfo.touchableRegion - ) - debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds) - + val touchableInsets: Int val bubbleBarVisible = controllers.bubbleControllers.isPresent && controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible() - var insetsIsTouchableRegion = true + // Prevents the taskbar from taking touches and conflicting with setup wizard if ( context.isPhoneButtonNavMode && + context.isUserSetupComplete && (!controllers.navbarButtonsViewController.isImeVisible || !controllers.navbarButtonsViewController.isImeRenderingNavButtons) ) { - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME) - insetsIsTouchableRegion = false + touchableInsets = TOUCHABLE_INSETS_FRAME + debugTouchableRegion.lastSetTouchableReason = PHONE_MODE } else if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) { // Let touches pass through us. - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) - debugTouchableRegion.lastSetTouchableReason = "Taskbar is invisible" - } else if ( - controllers.navbarButtonsViewController.isImeVisible && - controllers.taskbarStashController.isStashed - ) { - // Let touches pass through us. - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) - debugTouchableRegion.lastSetTouchableReason = "Stashed over IME" + touchableInsets = TOUCHABLE_INSETS_REGION + debugTouchableRegion.lastSetTouchableReason = DRAG_LAYER_INVISIBLE } else if (!controllers.uiController.isTaskbarTouchable) { // Let touches pass through us. - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) - debugTouchableRegion.lastSetTouchableReason = "Taskbar is not touchable" + touchableInsets = TOUCHABLE_INSETS_REGION + debugTouchableRegion.lastSetTouchableReason = UI_CONTROLLER_UNTOUCHABLE } else if (controllers.taskbarDragController.isSystemDragInProgress) { // Let touches pass through us. - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) - debugTouchableRegion.lastSetTouchableReason = "System drag is in progress" + touchableInsets = TOUCHABLE_INSETS_REGION + debugTouchableRegion.lastSetTouchableReason = SYSTEM_DRAG_IN_PROGRESS } else if (context.isTaskbarWindowFullscreen) { // Intercept entire fullscreen window. - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME) - insetsIsTouchableRegion = false - debugTouchableRegion.lastSetTouchableReason = "Taskbar is fullscreen" + touchableInsets = TOUCHABLE_INSETS_FRAME + debugTouchableRegion.lastSetTouchableReason = FULLSCREEN_TASKBAR_WINDOW context.dragLayer.getBoundsInWindow(debugTouchableRegion.lastSetTouchableBounds, false) } else if ( controllers.taskbarViewController.areIconsVisible() || @@ -412,10 +399,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas bubbleBarVisible ) { // Taskbar has some touchable elements, take over the full taskbar area - if ( - controllers.uiController.isInOverviewUi && - DisplayController.isTransientTaskbar(context) - ) { + if (controllers.uiController.isInOverviewUi && context.isTransientTaskbar) { val region = controllers.taskbarActivityContext.dragLayer.lastDrawnTransientRect.toRegion() val bubbleBarBounds = @@ -431,24 +415,30 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas // Include the bounds of the bubble bar in the touchable region if they exist. if (bubbleBarBounds != null) { - region.op(bubbleBarBounds, Region.Op.UNION) + region.addBoundsToRegion(bubbleBarBounds) } insetsInfo.touchableRegion.set(region) - debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview" + debugTouchableRegion.lastSetTouchableReason = TRANSIENT_IN_OVERVIEW debugTouchableRegion.lastSetTouchableBounds.set(region.bounds) } else { insetsInfo.touchableRegion.set(defaultTouchableRegion) - debugTouchableRegion.lastSetTouchableReason = "Using default touchable region" + debugTouchableRegion.lastSetTouchableReason = DEFAULT_TOUCH_REGION debugTouchableRegion.lastSetTouchableBounds.set(defaultTouchableRegion.bounds) } - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) - insetsIsTouchableRegion = false + touchableInsets = TOUCHABLE_INSETS_REGION } else { - insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) - debugTouchableRegion.lastSetTouchableReason = - "Icons are not visible, but other components such as 3 buttons might be" + touchableInsets = TOUCHABLE_INSETS_REGION + debugTouchableRegion.lastSetTouchableReason = ICONS_INVISIBLE } - context.excludeFromMagnificationRegion(insetsIsTouchableRegion) + // Always have nav buttons be touchable + controllers.navbarButtonsViewController.addVisibleButtonsRegion( + context.dragLayer, + insetsInfo.touchableRegion, + ) + + insetsInfo.setTouchableInsets(touchableInsets) + debugTouchableRegion.lastSetTouchableInsets = touchableInsets + debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds) } /** Draws the last set touchableRegion as a red rectangle onto the given Canvas. */ @@ -479,12 +469,42 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas } pw.println() } - pw.println("$prefix\tlastSetTouchableBounds=${debugTouchableRegion.lastSetTouchableBounds}") - pw.println("$prefix\tlastSetTouchableReason=${debugTouchableRegion.lastSetTouchableReason}") + pw.println("$prefix\tdebugTouchableRegion=$debugTouchableRegion") } class DebugTouchableRegion { + + companion object { + const val PHONE_MODE = + "Phone button nav mode: Fullscreen touchable, IME not affecting nav buttons" + const val DRAG_LAYER_INVISIBLE = "Taskbar is invisible" + const val UI_CONTROLLER_UNTOUCHABLE = "Taskbar is not touchable" + const val SYSTEM_DRAG_IN_PROGRESS = "System drag is in progress" + const val FULLSCREEN_TASKBAR_WINDOW = "Taskbar is fullscreen" + const val TRANSIENT_IN_OVERVIEW = "Transient Taskbar is in Overview" + const val DEFAULT_TOUCH_REGION = "Using default touchable region" + const val ICONS_INVISIBLE = + "Icons are not visible, but other components such as 3 buttons might be" + } + val lastSetTouchableBounds = Rect() var lastSetTouchableReason = "" + var lastSetTouchableInsets = TOUCHABLE_INSETS_FRAME + + override fun toString(): String { + return "{lastSetTouchableBounds=$lastSetTouchableBounds" + + ", lastSetTouchableReason=\"$lastSetTouchableReason\"" + + ", lastSetTouchableInsets=${touchableInsetsToString()}" + } + + private fun touchableInsetsToString(): String { + return when (lastSetTouchableInsets) { + TOUCHABLE_INSETS_FRAME -> "TOUCHABLE_INSETS_FRAME" + TOUCHABLE_INSETS_CONTENT -> "TOUCHABLE_INSETS_CONTENT" + TOUCHABLE_INSETS_VISIBLE -> "TOUCHABLE_INSETS_VISIBLE" + TOUCHABLE_INSETS_REGION -> "TOUCHABLE_INSETS_REGION" + else -> "Unknown" + } + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index cb9f24ae80..bf87d3e0df 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -16,16 +16,26 @@ package com.android.launcher3.taskbar; import static com.android.app.animation.Interpolators.EMPHASIZED; +import static com.android.app.animation.Interpolators.FINAL_FRAME; +import static com.android.app.animation.Interpolators.INSTANT; +import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; +import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT; +import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH; +import static com.android.launcher3.LauncherState.HOTSEAT_ICONS; +import static com.android.launcher3.Utilities.isRtl; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE; +import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_FOR_BUBBLES; +import static com.android.launcher3.taskbar.TaskbarStashController.UNLOCK_TRANSITION_MEMOIZATION_MS; import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME; +import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS; +import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS; import static com.android.launcher3.util.FlagDebugUtils.appendFlag; import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange; +import static com.android.quickstep.fallback.RecentsStateUtilsKt.toLauncherState; +import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK; -import static com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_AWAKE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -33,32 +43,44 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.os.SystemClock; import android.util.Log; +import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Hotseat; +import com.android.launcher3.Hotseat.HotseatQsbAlphaId; import com.android.launcher3.LauncherState; import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; +import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.OverviewComponentObserver; import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.RecentsAnimationController; +import com.android.quickstep.fallback.RecentsState; +import com.android.quickstep.fallback.window.RecentsWindowManager; +import com.android.quickstep.util.ScalingWorkspaceRevealAnim; import com.android.quickstep.util.SystemUiFlagUtils; import com.android.quickstep.views.RecentsView; import com.android.systemui.animation.ViewRootSync; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.HashMap; import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.function.Function; /** * Track LauncherState, RecentsAnimation, resumed state for task bar in one place here and animate @@ -143,16 +165,23 @@ public class TaskbarLauncherStateController { private AnimatedFloat mTaskbarBackgroundAlpha; private AnimatedFloat mTaskbarAlpha; private AnimatedFloat mTaskbarCornerRoundness; - private MultiProperty mIconAlphaForHome; + private MultiProperty mTaskbarAlphaForHome; + private @Nullable Animator mHotseatTranslationXAnimation; private QuickstepLauncher mLauncher; + private boolean mIsDestroyed = false; private Integer mPrevState; private int mState; private LauncherState mLauncherState = LauncherState.NORMAL; private boolean mSkipNextRecentsAnimEnd; // Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime (milliseconds). - private long mLastUnlockTimeMs = 0; + private long mLastRemoveTaskbarHiddenTimeMs = 0; + /** + * Time when FLAG_DEVICE_LOCKED was last cleared, plus + * {@link TaskbarStashController#UNLOCK_TRANSITION_MEMOIZATION_MS} + */ + private long mLastUnlockTransitionTimeout; private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener; @@ -160,11 +189,15 @@ public class TaskbarLauncherStateController { private boolean mShouldDelayLauncherStateAnim; + private @Nullable BubbleBarLocation mBubbleBarLocation; + // We skip any view synchronizations during init/destroy. private boolean mCanSyncViews; private boolean mIsQsbInline; + private RecentsAnimationCallbacks mRecentsAnimationCallbacks; + private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = new DeviceProfile.OnDeviceProfileChangeListener() { @Override @@ -172,16 +205,18 @@ public class TaskbarLauncherStateController { if (mIsQsbInline && !dp.isQsbInline) { // We only modify QSB alpha if isQsbInline = true. If we switch to a DP // where isQsbInline = false, then we need to reset the alpha. - mLauncher.getHotseat().setQsbAlpha(1f); + mLauncher.getHotseat().setQsbAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); } mIsQsbInline = dp.isQsbInline; TaskbarLauncherStateController.this.updateIconAlphaForHome( - mIconAlphaForHome.getValue()); + mTaskbarAlphaForHome.getValue(), ALPHA_CHANNEL_TASKBAR_ALIGNMENT); + TaskbarLauncherStateController.this.onBubbleBarLocationChanged( + mBubbleBarLocation, /* animate = */ false); } }; private final StateManager.StateListener mStateListener = - new StateManager.StateListener() { + new StateManager.StateListener<>() { @Override public void onStateTransitionStart(LauncherState toState) { @@ -195,7 +230,11 @@ public class TaskbarLauncherStateController { updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true); if (!mShouldDelayLauncherStateAnim) { if (toState == LauncherState.NORMAL) { - applyState(QuickstepTransitionManager.getTaskbarToHomeDuration()); + TaskbarActivityContext activity = mControllers.taskbarActivityContext; + boolean isPinnedTaskbarAndNotInDesktopMode = + !activity.isInDesktopMode() && activity.isPinnedTaskbar(); + applyState(QuickstepTransitionManager.getTaskbarToHomeDuration( + isPinnedTaskbarAndNotInDesktopMode)); } else { applyState(); } @@ -211,6 +250,20 @@ public class TaskbarLauncherStateController { } }; + private final StateManager.StateListener mRecentsStateListener = + new StateManager.StateListener<>() { + + @Override + public void onStateTransitionStart(RecentsState toState) { + mStateListener.onStateTransitionStart(toLauncherState(toState)); + } + + @Override + public void onStateTransitionComplete(RecentsState finalState) { + mStateListener.onStateTransitionComplete(toLauncherState(finalState)); + } + }; + /** * Callback for when launcher state transition completes after user swipes to home. * @param finalState The final state of the transition. @@ -239,31 +292,43 @@ public class TaskbarLauncherStateController { .getTaskbarBackgroundAlpha(); mTaskbarAlpha = mControllers.taskbarDragLayerController.getTaskbarAlpha(); mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness(); - mIconAlphaForHome = mControllers.taskbarViewController + mTaskbarAlphaForHome = mControllers.taskbarViewController .getTaskbarIconAlpha().get(ALPHA_INDEX_HOME); resetIconAlignment(); - mLauncher.getStateManager().addStateListener(mStateListener); + if (!mControllers.taskbarActivityContext.isPhoneMode()) { + mLauncher.getStateManager().addStateListener(mStateListener); + runForRecentsWindowManager(recentsWindowManager -> + recentsWindowManager.getStateManager().addStateListener(mRecentsStateListener)); + } mLauncherState = launcher.getStateManager().getState(); updateStateForSysuiFlags(sysuiStateFlags, /*applyState*/ false); applyState(0); - mCanSyncViews = true; + mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode(); mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); updateOverviewDragState(mLauncherState); } public void onDestroy() { + mIsDestroyed = true; mCanSyncViews = false; + if (mRecentsAnimationCallbacks != null) { + mRecentsAnimationCallbacks.removeListener(mTaskBarRecentsAnimationListener); + mRecentsAnimationCallbacks = null; + } + mIconAlignment.finishAnimation(); - mLauncher.getHotseat().setIconsAlpha(1f); + mLauncher.getHotseat().setIconsAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); mLauncher.getStateManager().removeStateListener(mStateListener); + runForRecentsWindowManager(recentsWindowManager -> + recentsWindowManager.getStateManager().removeStateListener(mRecentsStateListener)); - mCanSyncViews = true; + mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode(); mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); } @@ -278,6 +343,7 @@ public class TaskbarLauncherStateController { // If going to overview, stash the task bar // If going home, align the icons to hotseat AnimatorSet animatorSet = new AnimatorSet(); + mRecentsAnimationCallbacks = callbacks; // Update stashed flags first to ensure goingToUnstashedLauncherState() returns correctly. TaskbarStashController stashController = mControllers.taskbarStashController; @@ -289,21 +355,25 @@ public class TaskbarLauncherStateController { stashController.updateStateForFlag(FLAG_IN_APP, false); updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, true); + mLauncherState = toState; animatorSet.play(stashController.createApplyStateAnimator(duration)); animatorSet.play(applyState(duration, false)); if (mTaskBarRecentsAnimationListener != null) { mTaskBarRecentsAnimationListener.endGestureStateOverride( - !mLauncher.isInState(LauncherState.OVERVIEW), false /*canceled*/); + !isStateManagerInState(LauncherState.OVERVIEW), /* canceled= */ false); } mTaskBarRecentsAnimationListener = new TaskBarRecentsAnimationListener(callbacks); callbacks.addListener(mTaskBarRecentsAnimationListener); - ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(() -> - mTaskBarRecentsAnimationListener.endGestureStateOverride(true, false /*canceled*/)); + RecentsView recentsView = mControllers.uiController.getRecentsView(); + if (recentsView != null) { + recentsView.setTaskLaunchListener(() -> mTaskBarRecentsAnimationListener + .endGestureStateOverride(/* finishedToApp= */ true, /* canceled= */ false)); + recentsView.setTaskLaunchCancelledRunnable(() -> { + updateStateForUserFinishedToApp(/* finishedToApp= */ false); + }); + } - ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchCancelledRunnable(() -> { - updateStateForUserFinishedToApp(false /* finishedToApp */); - }); return animatorSet; } @@ -345,14 +415,7 @@ public class TaskbarLauncherStateController { updateStateForFlag(FLAG_DEVICE_LOCKED, SystemUiFlagUtils.isLocked(systemUiStateFlags)); - // Taskbar is hidden whenever the device is dreaming. The dreaming state includes the - // interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in - // when the device is asleep, the second condition extends ensures that the transition from - // and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar - // hide/reveal animation timings. - boolean isTaskbarHidden = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING) - || (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE; - updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden); + updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags)); if (applyState) { applyState(); @@ -365,10 +428,7 @@ public class TaskbarLauncherStateController { * @param launcherState The current state launcher is in */ private void updateOverviewDragState(LauncherState launcherState) { - boolean disallowLongClick = - FeatureFlags.enableSplitContextually() - ? mLauncher.isSplitSelectionActive() - : launcherState == LauncherState.OVERVIEW_SPLIT_SELECT; + boolean disallowLongClick = mLauncher.isSplitSelectionActive() || mIsAnimatingToLauncher; com.android.launcher3.taskbar.Utilities.setOverviewDragState( mControllers, launcherState.disallowTaskbarGlobalDrag(), disallowLongClick, launcherState.allowTaskbarInitialSplitSelection()); @@ -407,7 +467,7 @@ public class TaskbarLauncherStateController { } public Animator applyState(long duration, boolean start) { - if (mControllers.taskbarActivityContext.isDestroyed()) { + if (mIsDestroyed || mControllers.taskbarActivityContext.isPhoneMode()) { return null; } Animator animator = null; @@ -435,30 +495,54 @@ public class TaskbarLauncherStateController { private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) { final boolean isInLauncher = isInLauncher(); + final boolean isInOverview = mControllers.uiController.isInOverviewUi(); final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat(); final float toAlignment = isIconAlignedWithHotseat ? 1 : 0; boolean handleOpenFloatingViews = false; + boolean isPinnedTaskbar = + mControllers.taskbarActivityContext.isPinnedTaskbar(); if (DEBUG) { Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher + ", mLauncherState: " + mLauncherState + ", toAlignment: " + toAlignment); } mControllers.bubbleControllers.ifPresent(controllers -> { - // Show the bubble bar when on launcher home or in overview. - boolean onHome = isInLauncher && mLauncherState == LauncherState.NORMAL; - boolean onOverview = mLauncherState == LauncherState.OVERVIEW; - controllers.bubbleStashController.setBubblesShowingOnHome(onHome); - controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview); + // Ignore state changes when taskbar is destroyed + if (mControllers.taskbarActivityContext.isDestroyed()) { + return; + } + // Show the bubble bar when on launcher home (hotseat icons visible) or in overview + boolean onOverview = isInLauncher && mLauncherState == LauncherState.OVERVIEW; + boolean hotseatIconsVisible = isInLauncher && mLauncherState.areElementsVisible( + mLauncher, HOTSEAT_ICONS); + BubbleLauncherState state = onOverview + ? BubbleLauncherState.OVERVIEW + : hotseatIconsVisible + ? BubbleLauncherState.HOME + : BubbleLauncherState.IN_APP; + controllers.bubbleStashController.setLauncherState(state); }); - mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_OVERVIEW, + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, mLauncherState == LauncherState.OVERVIEW); + // Update taskbar stash flag here since we are skipping the playStateTransitionAnim below + if (isPinnedTaskbar) { + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, + mLauncherState.isTaskbarStashed(mLauncher)); + } + AnimatorSet animatorSet = new AnimatorSet(); if (hasAnyFlag(changedFlags, FLAG_LAUNCHER_IN_STATE_TRANSITION)) { boolean launcherTransitionCompleted = !hasAnyFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION); - playStateTransitionAnim(animatorSet, duration, launcherTransitionCompleted); + + // We are skipping the taskbar stash animation for pinned taskbar, as we handle that now + // in setupPinnedTaskbarAnimation. + if (!isPinnedTaskbar) { + playStateTransitionAnim(animatorSet, duration, launcherTransitionCompleted); + } if (launcherTransitionCompleted && mLauncherState == LauncherState.QUICK_SWITCH_FROM_HOME) { @@ -470,7 +554,8 @@ public class TaskbarLauncherStateController { // We're changing state to home, should close open popups e.g. Taskbar AllApps handleOpenFloatingViews = true; } - if (mLauncherState == LauncherState.OVERVIEW) { + if (mLauncherState == LauncherState.OVERVIEW + && !mControllers.taskbarActivityContext.isPhoneMode()) { // Calling to update the insets in TaskbarInsetController#updateInsetsTouchability mControllers.taskbarActivityContext.notifyUpdateLayoutParams(); } @@ -481,9 +566,9 @@ public class TaskbarLauncherStateController { @Override public void onAnimationStart(Animator animation) { mIsAnimatingToLauncher = isInLauncher; + // updateOverviewDragState uses mIsAnimatingToLauncher as well, so poke it. + updateOverviewDragState(mLauncherState); - TaskbarStashController stashController = - mControllers.taskbarStashController; if (DEBUG) { Log.d(TAG, "onAnimationStart - FLAG_IN_APP: " + !isInLauncher); } @@ -494,11 +579,15 @@ public class TaskbarLauncherStateController { @Override public void onAnimationEnd(Animator animation) { mIsAnimatingToLauncher = false; + // updateOverviewDragState uses mIsAnimatingToLauncher as well, so poke it. + updateOverviewDragState(mLauncherState); } }); // Handle closing open popups when going home/overview handleOpenFloatingViews = true; + } else { + stashController.applyState(); } if (handleOpenFloatingViews && isInLauncher) { @@ -507,7 +596,7 @@ public class TaskbarLauncherStateController { if (hasAnyFlag(changedFlags, FLAG_TASKBAR_HIDDEN) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) { // Take note of the current time, as the taskbar is made visible again. - mLastUnlockTimeMs = SystemClock.elapsedRealtime(); + mLastRemoveTaskbarHiddenTimeMs = SystemClock.elapsedRealtime(); } boolean isHidden = hasAnyFlag(FLAG_TASKBAR_HIDDEN); @@ -533,7 +622,8 @@ public class TaskbarLauncherStateController { // with a fingerprint reader. This should only be done when the device was woken // up via fingerprint reader, however since this information is currently not // available, opting to always delay the fade-in a bit. - long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() - mLastUnlockTimeMs; + long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() + - mLastRemoveTaskbarHiddenTimeMs; taskbarVisibility.setStartDelay( Math.max(0, TASKBAR_SHOW_DELAY_MS - durationSinceLastUnlockMs)); } @@ -541,10 +631,18 @@ public class TaskbarLauncherStateController { } float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1; + AnimatedFloat taskbarBgOffset = + mControllers.taskbarDragLayerController.getTaskbarBackgroundOffset(); + boolean showTaskbar = shouldShowTaskbar(mControllers.taskbarActivityContext, isInLauncher, + isInOverview) && !mControllers.taskbarStashController.isStashed(); + float taskbarBgOffsetEnd = showTaskbar ? 0f : 1f; + float taskbarBgOffsetStart = showTaskbar ? 1f : 0f; // Don't animate if background has reached desired value. if (mTaskbarBackgroundAlpha.isAnimating() - || mTaskbarBackgroundAlpha.value != backgroundAlpha) { + || mTaskbarBackgroundAlpha.value != backgroundAlpha + || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart) + || taskbarBgOffset.value != taskbarBgOffsetEnd) { mTaskbarBackgroundAlpha.cancelAnimation(); if (DEBUG) { Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - " @@ -555,30 +653,48 @@ public class TaskbarLauncherStateController { boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat; boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat; boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat; + // When Hotseat icons are not on top don't change duration or add start delay. + // This will keep the duration in sync for icon alignment and background fade in/out. + // For example, launching app from launcher all apps. + boolean isHotseatIconOnTopWhenAligned = + mControllers.uiController.isHotseatIconOnTopWhenAligned(); float startDelay = 0; // We want to delay the background from fading in so that the icons have time to move // into the bounds of the background before it appears. if (isInLauncherIconNotAligned) { startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT; - } else if (notInLauncherIconNotAligned) { + } else if (notInLauncherIconNotAligned && isHotseatIconOnTopWhenAligned) { startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT; } float newDuration = duration - startDelay; - if (isInLauncherIconIsAligned) { + if (isInLauncherIconIsAligned && isHotseatIconOnTopWhenAligned) { // Make the background fade out faster so that it is gone by the time the // icons move outside of the bounds of the background. newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT; } - Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha - .animateToValue(backgroundAlpha) - .setDuration((long) newDuration); - taskbarBackgroundAlpha.setStartDelay((long) startDelay); + Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha.animateToValue( + backgroundAlpha); + if (isPinnedTaskbar) { + setupPinnedTaskbarAnimation(animatorSet, showTaskbar, taskbarBgOffset, + taskbarBgOffsetStart, taskbarBgOffsetEnd, duration, taskbarBackgroundAlpha); + } else { + taskbarBackgroundAlpha.setDuration((long) newDuration); + taskbarBackgroundAlpha.setStartDelay((long) startDelay); + } animatorSet.play(taskbarBackgroundAlpha); } float cornerRoundness = isInLauncher ? 0 : 1; + if (mControllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview( + mControllers.taskbarActivityContext.getDisplayId()) + && mControllers.getSharedState() != null) { + cornerRoundness = + mControllers.taskbarDesktopModeController.getTaskbarCornerRoundness( + mControllers.getSharedState().showCornerRadiusInDesktopMode); + } + // Don't animate if corner roundness has reached desired value. if (mTaskbarCornerRoundness.isAnimating() || mTaskbarCornerRoundness.value != cornerRoundness) { @@ -596,6 +712,15 @@ public class TaskbarLauncherStateController { boolean isUnlockTransition = hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED) && !hasAnyFlag(FLAG_DEVICE_LOCKED); if (isUnlockTransition) { + // the launcher might not be resumed at the time the device is considered + // unlocked (when the keyguard goes away), but possibly shortly afterwards. + // To play the unlock transition at the time the unstash animation actually happens, + // this memoizes the state transition for UNLOCK_TRANSITION_MEMOIZATION_MS. + mLastUnlockTransitionTimeout = + SystemClock.elapsedRealtime() + UNLOCK_TRANSITION_MEMOIZATION_MS; + } + boolean isInUnlockTimeout = SystemClock.elapsedRealtime() < mLastUnlockTransitionTimeout; + if (isUnlockTransition || isInUnlockTimeout) { // When transitioning to unlocked, ensure the hotseat is fully visible from the // beginning. The hotseat itself is animated by LauncherUnlockAnimationController. mIconAlignment.cancelAnimation(); @@ -611,8 +736,11 @@ public class TaskbarLauncherStateController { } else if (mIconAlignment.isAnimatingToValue(toAlignment) || mIconAlignment.isSettledOnValue(toAlignment)) { // Already at desired value, but make sure we run the callback at the end. - animatorSet.addListener(AnimatorListeners.forEndCallback( - this::onIconAlignmentRatioChanged)); + animatorSet.addListener(AnimatorListeners.forEndCallback(() -> { + if (!mIconAlignment.isAnimating()) { + onIconAlignmentRatioChanged(); + } + })); } else { mIconAlignment.cancelAnimation(); ObjectAnimator iconAlignAnim = mIconAlignment @@ -623,10 +751,19 @@ public class TaskbarLauncherStateController { + mIconAlignment.value + " -> " + toAlignment + ": " + duration); } - animatorSet.play(iconAlignAnim); + if (!isPinnedTaskbar) { + if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) { + iconAlignAnim.setInterpolator(FINAL_FRAME); + } else { + animatorSet.play(iconAlignAnim); + } + } } - animatorSet.setInterpolator(EMPHASIZED); + Interpolator interpolator = enableScalingRevealHomeAnimation() && !isPinnedTaskbar + ? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED; + + animatorSet.setInterpolator(interpolator); if (start) { animatorSet.start(); @@ -634,12 +771,81 @@ public class TaskbarLauncherStateController { return animatorSet; } + private static boolean shouldShowTaskbar(TaskbarActivityContext activityContext, + boolean isInLauncher, boolean isInOverview) { + if (activityContext.showDesktopTaskbarForFreeformDisplay()) { + return true; + } + + if (activityContext.showLockedTaskbarOnHome() && isInLauncher) { + return true; + } + return !isInLauncher || isInOverview; + } + + // Used to stash/unstash pinned taskbar between home, overview, in app states. + private void setupPinnedTaskbarAnimation(AnimatorSet animatorSet, boolean showTaskbar, + AnimatedFloat taskbarBgOffset, float taskbarBgOffsetStart, float taskbarBgOffsetEnd, + long duration, Animator taskbarBackgroundAlpha) { + float targetAlpha = !showTaskbar ? 1 : 0; + mLauncher.getHotseat().setIconsAlpha(targetAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); + if (mIsQsbInline) { + mLauncher.getHotseat().setQsbAlpha(targetAlpha, + ALPHA_CHANNEL_TASKBAR_ALIGNMENT); + } + + float targetTaskbarIconAlpha = showTaskbar ? 1f : 0f; + if (mTaskbarAlphaForHome.getValue() != targetTaskbarIconAlpha) { + animatorSet.play(mTaskbarAlphaForHome + .animateToValue(targetTaskbarIconAlpha) + .setDuration(duration)); + } + if ((taskbarBgOffset.value != taskbarBgOffsetEnd && !taskbarBgOffset.isAnimating()) + || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)) { + taskbarBgOffset.cancelAnimation(); + AnimatedFloat taskbarIconTranslationYForHome = + mControllers.taskbarViewController.mTaskbarIconTranslationYForHome; + ObjectAnimator taskbarBackgroundOffset = taskbarBgOffset.animateToValue( + taskbarBgOffsetStart, + taskbarBgOffsetEnd); + ObjectAnimator taskbarIconsYTranslation = null; + float taskbarHeight = mControllers + .taskbarActivityContext + .getDeviceProfile() + .getTaskbarProfile() + .getHeight(); + if (showTaskbar) { + taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue( + taskbarHeight, 0); + } else { + taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(0, + taskbarHeight); + } + + taskbarIconsYTranslation.setDuration(duration); + taskbarBackgroundOffset.setDuration(duration); + + animatorSet.play(taskbarIconsYTranslation); + animatorSet.play(taskbarBackgroundOffset); + } + taskbarBackgroundAlpha.setInterpolator(showTaskbar ? INSTANT : FINAL_FRAME); + taskbarBackgroundAlpha.setDuration(duration); + } + /** * Whether the taskbar is aligned with the hotseat in the current/target launcher state. * * This refers to the intended state - a transition to this state might be in progress. */ public boolean isTaskbarAlignedWithHotseat() { + if (mControllers.taskbarActivityContext.showDesktopTaskbarForFreeformDisplay()) { + return false; + } + + if (mControllers.taskbarActivityContext.showLockedTaskbarOnHome() && isInLauncher()) { + return false; + } + return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher); } @@ -651,8 +857,7 @@ public class TaskbarLauncherStateController { boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher); boolean willStashVisually = isInStashedState && mControllers.taskbarStashController.supportsVisualStashing(); - boolean isTaskbarAlignedWithHotseat = - mLauncherState.isTaskbarAlignedWithHotseat(mLauncher); + boolean isTaskbarAlignedWithHotseat = isTaskbarAlignedWithHotseat(); return isTaskbarAlignedWithHotseat && !willStashVisually; } else { return false; @@ -671,6 +876,15 @@ public class TaskbarLauncherStateController { return mLauncherState.isRecentsViewVisible; } + /** + * Returns the current mLauncherState. Note that this could represent RecentsState as well, as + * we convert those to equivalent LauncherStates even if Launcher Activity is not actually in + * those states (for the case where the state is represented in a separate Window instead). + */ + public LauncherState getLauncherState() { + return mLauncherState; + } + private void playStateTransitionAnim(AnimatorSet animatorSet, long duration, boolean committed) { boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher); @@ -683,14 +897,17 @@ public class TaskbarLauncherStateController { public void onAnimationEnd(Animator animation) { if (isInStashedState && committed) { // Reset hotseat alpha to default - mLauncher.getHotseat().setIconsAlpha(1); + mLauncher.getHotseat().setIconsAlpha(1, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); } } @Override public void onAnimationStart(Animator animation) { - if (mLauncher.getHotseat().getIconsAlpha() > 0) { - updateIconAlphaForHome(mLauncher.getHotseat().getIconsAlpha()); + float hotseatIconsAlpha = mLauncher.getHotseat() + .getIconsAlpha(ALPHA_CHANNEL_TASKBAR_ALIGNMENT) + .getValue(); + if (hotseatIconsAlpha > 0) { + updateIconAlphaForHome(hotseatIconsAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT); } } }); @@ -719,6 +936,36 @@ public class TaskbarLauncherStateController { } } + protected void stashHotseat(boolean stash) { + // align taskbar with the hotseat icons before performing any animation + mControllers.taskbarViewController.setLauncherIconAlignment(/* alignmentRatio = */ 1, + mLauncher.getDeviceProfile()); + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, stash); + Runnable swapHotseatWithTaskbar = new Runnable() { + @Override + public void run() { + updateIconAlphaForHome(stash ? 1 : 0, ALPHA_CHANNEL_TASKBAR_STASH); + } + }; + if (stash) { + stashController.applyState(); + // if we stashing the hotseat we need to immediately swap it with the animating taskbar + swapHotseatWithTaskbar.run(); + } else { + // if we revert stashing make swap after taskbar animation is complete + stashController.applyState(/* postApplyAction = */ swapHotseatWithTaskbar); + } + } + + protected void unStashHotseatInstantly() { + TaskbarStashController stashController = mControllers.taskbarStashController; + stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, false); + stashController.applyState(/* duration = */ 0); + updateIconAlphaForHome(/* taskbarAlpha = */ 0, + ALPHA_CHANNEL_TASKBAR_STASH, /* updateTaskbarAlpha = */ false); + } + /** * Resets and updates the icon alignment. */ @@ -728,7 +975,7 @@ public class TaskbarLauncherStateController { } private void onIconAlignmentRatioChanged() { - float currentValue = mIconAlphaForHome.getValue(); + float currentValue = mTaskbarAlphaForHome.getValue(); boolean taskbarWillBeVisible = mIconAlignment.value < 1; boolean firstFrameVisChanged = (taskbarWillBeVisible && Float.compare(currentValue, 1) != 0) || (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0); @@ -736,24 +983,33 @@ public class TaskbarLauncherStateController { mControllers.taskbarViewController.setLauncherIconAlignment( mIconAlignment.value, mLauncher.getDeviceProfile()); mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value); - // Switch taskbar and hotseat in last frame - updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0); + // Switch taskbar and hotseat in last frame and if taskbar is not hidden for bubbles + boolean isHiddenForBubbles = mControllers.taskbarStashController.isHiddenForBubbles(); + updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0, ALPHA_CHANNEL_TASKBAR_ALIGNMENT, + /* updateTaskbarAlpha = */ !isHiddenForBubbles); // Sync the first frame where we swap taskbar and hotseat. if (firstFrameVisChanged && mCanSyncViews && !Utilities.isRunningInTestHarness()) { ViewRootSync.synchronizeNextDraw(mLauncher.getHotseat(), mControllers.taskbarActivityContext.getDragLayer(), - () -> { - }); + () -> {}); } } - private void updateIconAlphaForHome(float alpha) { - if (mControllers.taskbarActivityContext.isDestroyed()) { + private void updateIconAlphaForHome(float taskbarAlpha, @HotseatQsbAlphaId int alphaChannel) { + updateIconAlphaForHome(taskbarAlpha, alphaChannel, /* updateTaskbarAlpha = */ true); + } + + private void updateIconAlphaForHome(float taskbarAlpha, + @HotseatQsbAlphaId int alphaChannel, + boolean updateTaskbarAlpha) { + if (mIsDestroyed) { return; } - mIconAlphaForHome.setValue(alpha); - boolean hotseatVisible = alpha == 0 + if (updateTaskbarAlpha) { + mTaskbarAlphaForHome.setValue(taskbarAlpha); + } + boolean hotseatVisible = taskbarAlpha == 0 || mControllers.taskbarActivityContext.isPhoneMode() || (!mControllers.uiController.isHotseatIconOnTopWhenAligned() && mIconAlignment.value > 0); @@ -761,12 +1017,77 @@ public class TaskbarLauncherStateController { * Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets * should not be visible at the same time. */ - mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0); + float targetAlpha = hotseatVisible ? 1 : 0; + mLauncher.getHotseat().setIconsAlpha(targetAlpha, alphaChannel); if (mIsQsbInline) { - mLauncher.getHotseat().setQsbAlpha(hotseatVisible ? 1 : 0); + mLauncher.getHotseat().setQsbAlpha(targetAlpha, alphaChannel); } } + /** Updates launcher home screen appearance accordingly to the bubble bar location. */ + public void onBubbleBarLocationChanged(@Nullable BubbleBarLocation location, boolean animate) { + mBubbleBarLocation = location; + if (location == null) { + // bubble bar is not present, hence no location, resetting the hotseat + updateHotseatAndQsbTranslationX(/* targetValue = */ 0, animate); + mBubbleBarLocation = null; + return; + } + DeviceProfile deviceProfile = mLauncher.getDeviceProfile(); + if (!deviceProfile.shouldAdjustHotseatOnNavBarLocationUpdate( + mControllers.taskbarActivityContext)) { + return; + } + boolean isBubblesOnLeft = location.isOnLeft(isRtl(mLauncher.getResources())); + int targetX = deviceProfile + .getHotseatTranslationXForNavBar(mLauncher, isBubblesOnLeft); + updateHotseatAndQsbTranslationX(targetX, animate); + } + + /** Used to translate hotseat and QSB to make room for bubbles. */ + private void updateHotseatAndQsbTranslationX(float targetValue, boolean animate) { + // cancel existing animation + if (mHotseatTranslationXAnimation != null) { + mHotseatTranslationXAnimation.cancel(); + mHotseatTranslationXAnimation = null; + } + Hotseat hotseat = mLauncher.getHotseat(); + AnimatorSet translationXAnimation = new AnimatorSet(); + MultiProperty iconsTranslationX = mLauncher.getHotseat() + .getIconsTranslationX(Hotseat.ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT); + if (animate) { + translationXAnimation.playTogether(iconsTranslationX.animateToValue(targetValue)); + } else { + iconsTranslationX.setValue(targetValue); + } + float qsbTargetX = 0; + if (mIsQsbInline) { + qsbTargetX = targetValue; + } + MultiProperty qsbTranslationX = hotseat.getQsbTranslationX(); + if (qsbTranslationX != null) { + if (animate) { + translationXAnimation.playTogether(qsbTranslationX.animateToValue(qsbTargetX)); + } else { + qsbTranslationX.setValue(qsbTargetX); + } + } + if (!animate) { + return; + } + mHotseatTranslationXAnimation = translationXAnimation; + translationXAnimation.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS); + translationXAnimation.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS); + translationXAnimation.setInterpolator(Interpolators.EMPHASIZED); + translationXAnimation.start(); + } + + private boolean isStateManagerInState(@NonNull LauncherState state) { + return mLauncher.isInState(state) || state == getFromRecentsWindowManager( + recentsWindowManager -> + toLauncherState(recentsWindowManager.getStateManager().getState())); + } + private final class TaskBarRecentsAnimationListener implements RecentsAnimationCallbacks.RecentsAnimationListener { private final RecentsAnimationCallbacks mCallbacks; @@ -777,13 +1098,13 @@ public class TaskbarLauncherStateController { @Override public void onRecentsAnimationCanceled(HashMap thumbnailDatas) { - boolean isInOverview = mLauncher.isInState(LauncherState.OVERVIEW); - endGestureStateOverride(!isInOverview, true /*canceled*/); + boolean isInOverview = isStateManagerInState(LauncherState.OVERVIEW); + endGestureStateOverride(!isInOverview, /* canceled= */ true); } @Override public void onRecentsAnimationFinished(RecentsAnimationController controller) { - endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/); + endGestureStateOverride(!controller.getFinishTargetIsLauncher(), /* canceled= */ false); } /** @@ -793,13 +1114,18 @@ public class TaskbarLauncherStateController { * * @param finishedToApp {@code true} if the recents animation finished to showing an app and * not workspace or overview - * @param canceled {@code true} if the recents animation was canceled instead of finishing - * to completion + * @param canceled {@code true} if the recents animation was canceled instead of + * finishing + * to completion */ private void endGestureStateOverride(boolean finishedToApp, boolean canceled) { mCallbacks.removeListener(this); mTaskBarRecentsAnimationListener = null; - ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null); + RecentsView recentsView = mControllers.uiController.getRecentsView(); + if (recentsView != null) { + recentsView.setTaskLaunchListener(null); + recentsView.setTaskLaunchCancelledRunnable(null); + } if (mSkipNextRecentsAnimEnd && !canceled) { mSkipNextRecentsAnimEnd = false; @@ -811,6 +1137,7 @@ public class TaskbarLauncherStateController { /** * Updates the visible state immediately to ensure a seamless handoff. + * * @param finishedToApp True iff user is in an app. */ private void updateStateForUserFinishedToApp(boolean finishedToApp) { @@ -828,6 +1155,30 @@ public class TaskbarLauncherStateController { controller.applyState(); } + /** + * Helper function to run a callback on the RecentsWindowManager (if it exists). + */ + private void runForRecentsWindowManager(Consumer callback) { + getFromRecentsWindowManager(recentsWindowManager -> { + callback.accept(recentsWindowManager); + return null; + }); + } + + private @Nullable T getFromRecentsWindowManager( + Function function) { + final TaskbarActivityContext taskbarContext = mControllers.taskbarActivityContext; + int displayId = taskbarContext.getDisplayId(); + BaseContainerInterface containerInterface = OverviewComponentObserver.INSTANCE.get( + taskbarContext).getContainerInterface(displayId); + if (containerInterface == null + || !(containerInterface.getCreatedContainer() instanceof RecentsWindowManager + recentsWindowManager)) { + return null; + } + return function.apply(recentsWindowManager); + } + private static String getStateString(int flags) { StringJoiner result = new StringJoiner("|"); appendFlag(result, flags, FLAG_VISIBLE, "flag_visible"); @@ -851,8 +1202,9 @@ public class TaskbarLauncherStateController { pw.println(String.format( "%s\tmTaskbarBackgroundAlpha=%.2f", prefix, mTaskbarBackgroundAlpha.value)); pw.println(String.format( - "%s\tmIconAlphaForHome=%.2f", prefix, mIconAlphaForHome.getValue())); - pw.println(String.format("%s\tmPrevState=%s", prefix, getStateString(mPrevState))); + "%s\tmTaskbarAlphaForHome=%.2f", prefix, mTaskbarAlphaForHome.getValue())); + pw.println(String.format("%s\tmPrevState=%s", prefix, + mPrevState == null ? null : getStateString(mPrevState))); pw.println(String.format("%s\tmState=%s", prefix, getStateString(mState))); pw.println(String.format("%s\tmLauncherState=%s", prefix, mLauncherState)); pw.println(String.format( diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.kt new file mode 100644 index 0000000000..36929c524a --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.app.PendingIntent +import com.android.app.displaylib.DisplayDecorationListener +import com.android.launcher3.anim.AnimatorPlaybackController +import com.android.launcher3.statemanager.StatefulActivity +import com.android.quickstep.views.RecentsViewContainer +import com.android.systemui.shared.statusbar.phone.BarTransitions +import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags +import java.io.PrintWriter + +interface TaskbarManager : DisplayDecorationListener { + + fun createLauncherStartFromSuwAnim(duration: Int): AnimatorPlaybackController? + + fun onUserUnlocked() + + fun setActivity(activity: StatefulActivity<*>) + + fun setRecentsViewContainer(recentsViewContainer: RecentsViewContainer) + + fun recreateTaskbars() + + fun onSystemUiFlagsChanged(@SystemUiStateFlags systemUiStateFlags: Long, displayId: Int) + + fun onLongPressHomeEnabled(assistantLongPressEnabled: Boolean) + + fun setSetupUIVisible(isVisible: Boolean) + + fun setWallpaperVisible(displayId: Int, isVisible: Boolean) + + fun checkNavBarModes(displayId: Int) + + fun finishBarAnimations(displayId: Int) + + fun touchAutoDim(displayId: Int, reset: Boolean) + + fun transitionTo(displayId: Int, @BarTransitions.TransitionMode barMode: Int, animate: Boolean) + + fun appTransitionPending(pending: Boolean) + + fun onRotationProposal(rotation: Int, isValid: Boolean) + + fun disableNavBarElements(displayId: Int, state1: Int, state2: Int, animate: Boolean) + + fun onSystemBarAttributesChanged(displayId: Int, behavior: Int) + + fun onTransitionModeUpdated(barMode: Int, checkBarModes: Boolean) + + fun onNavButtonsDarkIntensityChanged(darkIntensity: Float) + + fun onNavigationBarLumaSamplingEnabled(displayId: Int, enable: Boolean) + + fun destroy() + + fun getCurrentActivityContext(): TaskbarActivityContext? + + fun dumpLogs(prefix: String, pw: PrintWriter) + + fun getUIControllerForDisplay(displayId: Int): TaskbarUIController? + + fun getTaskbarForDisplay(displayId: Int): TaskbarActivityContext? + + fun createAllAppsPendingIntent(): PendingIntent + + fun getPrimaryDisplayId(): Int + + fun debugPrimaryTaskbar(debugReason: String, verbose: Boolean) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManagerImpl.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManagerImpl.java new file mode 100644 index 0000000000..81dc30e344 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManagerImpl.java @@ -0,0 +1,1962 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar; + +import static android.content.Context.RECEIVER_EXPORTED; +import static android.content.Context.RECEIVER_NOT_EXPORTED; +import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; +import static android.content.pm.PackageManager.FEATURE_PC; +import static android.os.Process.THREAD_PRIORITY_FOREGROUND; +import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; + +import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; +import static com.android.launcher3.Flags.enableGrowthNudge; +import static com.android.launcher3.Flags.enableTaskbarUiThread; +import static com.android.launcher3.Flags.enableUnfoldStateAnimation; +import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; +import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate; +import static com.android.launcher3.taskbar.growth.GrowthConstants.BROADCAST_SHOW_NUDGE; +import static com.android.launcher3.taskbar.growth.GrowthConstants.GROWTH_NUDGE_PERMISSION; +import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; +import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE; +import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; +import static com.android.launcher3.util.DisplayController.CHANGE_SHOW_LOCKED_TASKBAR; +import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange; +import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR; +import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR; +import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops; + +import android.animation.AnimatorSet; +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.IIntentReceiver; +import android.content.IIntentSender; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.hardware.display.DisplayManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Trace; +import android.provider.Settings; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.view.Display; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.window.DesktopExperienceFlags; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.app.displaylib.DisplayDecorationListener; +import com.android.app.displaylib.DisplaysWithDecorationsRepositoryCompat; +import com.android.app.displaylib.PerDisplayRepository; +import com.android.internal.util.LatencyTracker; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.anim.AnimatorListeners; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.statehandlers.DesktopVisibilityController; +import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks; +import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.LooperExecutor; +import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.util.SimpleBroadcastReceiver; +import com.android.quickstep.AllAppsActionManager; +import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.OverviewComponentObserver; +import com.android.quickstep.RecentsActivity; +import com.android.quickstep.SystemDecorationChangeObserver; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.fallback.window.RecentsWindowManager; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.views.RecentsViewContainer; +//import com.android.server.am.Flags; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.statusbar.phone.BarTransitions; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.systemui.unfold.UnfoldTransitionProgressProvider; +import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; + +import kotlinx.coroutines.CoroutineDispatcher; + +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.function.IntConsumer; + +import app.lawnchair.LawnchairApp; + +/** + * Class to manage taskbar lifecycle + */ +public class TaskbarManagerImpl implements DisplayDecorationListener { + private static final String TAG = "TaskbarManager"; + private static final boolean DEBUG = false; + private static final int TASKBAR_DESTROY_DURATION = 100; + + // TODO: b/397738606 - Remove all logs with this tag after the growth framework is integrated. + public static final String GROWTH_FRAMEWORK_TAG = "Growth Framework"; + + /** + * All the configurations which do not initiate taskbar recreation. + * This includes all the configurations defined in Launcher's manifest entry and + * ActivityController#filterConfigChanges + */ + private static final int SKIP_RECREATE_CONFIG_CHANGES = ActivityInfo.CONFIG_WINDOW_CONFIGURATION + | ActivityInfo.CONFIG_KEYBOARD + | ActivityInfo.CONFIG_KEYBOARD_HIDDEN + | ActivityInfo.CONFIG_MCC + | ActivityInfo.CONFIG_MNC + | ActivityInfo.CONFIG_NAVIGATION + | ActivityInfo.CONFIG_ORIENTATION + | ActivityInfo.CONFIG_SCREEN_SIZE + | ActivityInfo.CONFIG_SCREEN_LAYOUT + | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; + + private static final Uri USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor( + Settings.Secure.USER_SETUP_COMPLETE); + + private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor( + Settings.Secure.NAV_BAR_KIDS_MODE); + + private static final LooperExecutor TASKBAR_UI_THREAD = + new LooperExecutor("TASKBAR_UI_THREAD", THREAD_PRIORITY_FOREGROUND); + + private final Context mBaseContext; + private final int mPrimaryDisplayId; + private final TaskbarNavButtonCallbacks mNavCallbacks; + // TODO: Remove this during the connected displays lifecycle refactor. + private final Context mPrimaryWindowContext; + private final WindowManager mPrimaryWindowManager; + private final DisplayManager mDisplayManager; + private TaskbarNavButtonController mPrimaryNavButtonController; + private ComponentCallbacks mPrimaryComponentCallbacks; + + private final SimpleBroadcastReceiver mShutdownReceiver; + private final DisplaysWithDecorationsRepositoryCompat mDisplaysWithDecorationsRepositoryCompat; + + // The source for this provider is set when Launcher is available + // We use 'non-destroyable' version here so the original provider won't be destroyed + // as it is tied to the activity lifecycle, not the taskbar lifecycle. + // It's destruction/creation will be managed by the activity. + private final ScopedUnfoldTransitionProgressProvider mUnfoldProgressProvider = + new NonDestroyableScopedUnfoldTransitionProgressProvider(); + /** DisplayId - {@link TaskbarActivityContext} map for Connected Display. */ + private final Map + mTaskbars = enableTaskbarUiThread() ? new ConcurrentHashMap<>() : new HashMap<>(); + /** DisplayId - {@link Context} map for Connected Display. */ + private final SparseArray mWindowContexts = new SparseArray<>(); + /** DisplayId - {@link FrameLayout} map for Connected Display. */ + private final SparseArray mRootLayouts = new SparseArray<>(); + /** DisplayId - {@link Boolean} map indicating if RootLayout was added to window. */ + private final SparseBooleanArray mAddedRootLayouts = new SparseBooleanArray(); + /** DisplayId - {@link TaskbarNavButtonController} map for Connected Display. */ + private final SparseArray mNavButtonControllers = + new SparseArray<>(); + /** DisplayId - {@link ComponentCallbacks} map for Connected Display. */ + private final SparseArray mComponentCallbacks = new SparseArray<>(); + /** DisplayId - {@link deviceprofile} map for Connected Display. */ + private final SparseArray mExternalDeviceProfiles = new SparseArray<>(); + private StatefulActivity mActivity; + private RecentsViewContainer mRecentsViewContainer; + /** Whether this device is a desktop android device **/ + private boolean mIsAndroidPC; + /** Whether this device supports freeform windows management. Can change dynamically **/ + private boolean mSupportsFreeformWindowsManagement; + + /** + * Cache a copy here so we can initialize state whenever taskbar is recreated, since + * this class does not get re-initialized w/ new taskbars. + */ + private final Map mTaskbarSharedStates = new ConcurrentHashMap<>(); + + /** + * We use WindowManager's ComponentCallbacks() for internal UI changes (similar to an Activity) + * which comes via a different channel + */ + private final RecreationListener mRecreationListener = new RecreationListener(); + + // Currently, there is a duplicative call to recreate taskbars when user enter/exit Desktop + // Mode upon getting transition callback from shell side. So, we make sure that if taskbar is + // already in recreate process due to transition callback, don't recreate for + // DisplayInfoChangeListener. + private boolean mShouldIgnoreNextDesktopModeChangeFromDisplayControllerForPrimary = false; + + private class RecreationListener implements DisplayController.DisplayInfoChangeListener { + @Override + public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) { + int displayId = context.getDisplayId(); + if ((flags & CHANGE_DENSITY) != 0) { + debugTaskbarManager("onDisplayInfoChanged: Display density changed", displayId); + } + if ((flags & CHANGE_NAVIGATION_MODE) != 0) { + debugTaskbarManager("onDisplayInfoChanged: Navigation mode changed", displayId); + } + if ((flags & CHANGE_DESKTOP_MODE) != 0) { + debugTaskbarManager("onDisplayInfoChanged: Desktop mode changed", displayId); + handleDisplayUpdatesForPerceptibleTasks(); + } + if ((flags & CHANGE_TASKBAR_PINNING) != 0) { + debugTaskbarManager("onDisplayInfoChanged: Taskbar pinning changed", displayId); + } + + // Use a helper to update DP (only for secondary displays) and then recreate taskbar. + IntConsumer updateExternalDpAndRecreateTaskbar = displayIdToUpdate -> { + // Don't update DP for primary display as IDP already takes care of this. + createExternalDeviceProfile(displayIdToUpdate); + recreateTaskbarForDisplay(displayIdToUpdate, /* duration= */ 0); + }; + + if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE | CHANGE_DESKTOP_MODE + | CHANGE_TASKBAR_PINNING | CHANGE_SHOW_LOCKED_TASKBAR)) != 0) { + + TaskbarActivityContext taskbarActivityContext = getTaskbarForDisplay(displayId); + if ((flags & CHANGE_SHOW_LOCKED_TASKBAR) != 0) { + debugTaskbarManager("onDisplayInfoChanged: show locked taskbar changed!", + displayId); + updateExternalDpAndRecreateTaskbar.accept(displayId); + } else if ((flags & CHANGE_DESKTOP_MODE) != 0) { + if (displayId == mPrimaryDisplayId + && mShouldIgnoreNextDesktopModeChangeFromDisplayControllerForPrimary) { + mShouldIgnoreNextDesktopModeChangeFromDisplayControllerForPrimary = false; + return; + } + // Only Handles Special Exit Cases for Desktop Mode Taskbar Recreation. + if (((flags & CHANGE_TASKBAR_PINNING) != 0) || (taskbarActivityContext != null + && !taskbarActivityContext.showLockedTaskbarOnHome() + && !taskbarActivityContext.showDesktopTaskbarForFreeformDisplay())) { + updateExternalDpAndRecreateTaskbar.accept(displayId); + } + } else { + updateExternalDpAndRecreateTaskbar.accept(displayId); + } + } + } + } + + private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> { + debugPrimaryTaskbar("Settings changed! Recreating Taskbar!"); + recreateTaskbars(); + }; + + private PerceptibleTaskListener mTaskStackListener; + + private class PerceptibleTaskListener implements TaskStackChangeListener { + private ArraySet mPerceptibleTasks = new ArraySet(); + + @Override + public void onTaskMovedToFront(int taskId) { + // This listens to any Task, so we filter them by the ones shown in the launcher. + // For Tasks restored after startup, they will by default not be Perceptible, and no + // need to until user interacts with it by bringing it to the foreground. + for (Entry entry : mTaskbars.entrySet()) { + // get pinned tasks - we care about all tasks, not just the one moved to the front + Set taskbarPinnedTasks = + entry.getValue().getControllers().taskbarViewController + .getShownTaskIds(); + + // filter out tasks already marked as perceptible + taskbarPinnedTasks.removeAll(mPerceptibleTasks); + + // add the filtered tasks as perceptible + for (int pinnedTaskId : taskbarPinnedTasks) { + ActivityManagerWrapper.getInstance() + .setTaskIsPerceptible(pinnedTaskId, true); + mPerceptibleTasks.add(pinnedTaskId); + } + } + } + + /** + * Launcher also can display recently launched tasks that are not pinned. Also add + * these as perceptible + */ + @Override + public void onRecentTaskListUpdated() { + for (Entry entry : mTaskbars.entrySet()) { + for (GroupTask gTask : entry.getValue().getControllers() + .taskbarRecentAppsController.getShownTasks()) { + for (Task task : gTask.getTasks()) { + int taskId = task.key.id; + + if (!mPerceptibleTasks.contains(taskId)) { + ActivityManagerWrapper.getInstance() + .setTaskIsPerceptible(taskId, true); + mPerceptibleTasks.add(taskId); + } + } + } + } + } + + @Override + public void onTaskRemoved(int taskId) { + mPerceptibleTasks.remove(taskId); + } + + public void unregisterListener() { + for (Integer taskId : mPerceptibleTasks) { + ActivityManagerWrapper.getInstance().setTaskIsPerceptible(taskId, false); + } + TaskStackChangeListeners.getInstance().unregisterTaskStackListener( + mTaskStackListener); + } + } + + private final DesktopVisibilityController.TaskbarDesktopModeListener + mTaskbarDesktopModeListener = + new DesktopVisibilityController.TaskbarDesktopModeListener() { + @Override + public void onExitDesktopMode(int duration) { + if (enableMultipleDesktops(mBaseContext)) { + LatencyTracker.getInstance(mBaseContext).onActionStart( + LatencyTracker.ACTION_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE); + } + + TaskbarActivityContext taskbarActivityContext = getCurrentActivityContext(); + if (taskbarActivityContext != null + && !taskbarActivityContext.isInOverview() + && !taskbarActivityContext.showDesktopTaskbarForFreeformDisplay()) { + mShouldIgnoreNextDesktopModeChangeFromDisplayControllerForPrimary = true; + AnimatorSet animatorSet = taskbarActivityContext.onDestroyAnimation( + TASKBAR_DESTROY_DURATION); + animatorSet.addListener(AnimatorListeners.forEndCallback( + () -> recreateTaskbarForDisplay(mPrimaryDisplayId, duration))); + animatorSet.start(); + } + } + + @Override + public void onEnterDesktopMode(int duration) { + TaskbarActivityContext taskbarActivityContext = getCurrentActivityContext(); + if (taskbarActivityContext != null + && !taskbarActivityContext.showDesktopTaskbarForFreeformDisplay()) { + mShouldIgnoreNextDesktopModeChangeFromDisplayControllerForPrimary = true; + AnimatorSet animatorSet = taskbarActivityContext.onDestroyAnimation( + TASKBAR_DESTROY_DURATION); + animatorSet.addListener(AnimatorListeners.forEndCallback( + () -> recreateTaskbarForDisplay(mPrimaryDisplayId, duration))); + animatorSet.start(); + } + } + + @Override + public void onTaskbarCornerRoundingUpdate( + boolean doesAnyTaskRequireTaskbarRounding) { + //NO-OP + } + }; + + private boolean mUserUnlocked = false; + + private final Map mTaskbarBroadcastReceivers = + new ConcurrentHashMap<>(); + + private final SimpleBroadcastReceiver mGrowthBroadcastReceiver; + + private final AllAppsActionManager mAllAppsActionManager; + private final PerDisplayRepository mRecentsWindowManagerRepository; + + private final Runnable mActivityOnDestroyCallback = new Runnable() { + @Override + public void run() { + int displayId = mPrimaryDisplayId; + debugTaskbarManager("onActivityDestroyed:", displayId); + if (mActivity != null) { + displayId = mActivity.getDisplayId(); + mActivity.removeOnDeviceProfileChangeListener( + mDebugActivityDeviceProfileChanged); + debugTaskbarManager("onActivityDestroyed: unregistering callbacks", displayId); + mActivity.removeEventCallback(EVENT_DESTROYED, this); + } + if (mActivity == mRecentsViewContainer) { + mRecentsViewContainer = null; + } + mActivity = null; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + debugTaskbarManager("onActivityDestroyed: setting taskbarUIController", displayId); + taskbar.setUIController(TaskbarUIController.DEFAULT); + } else { + debugTaskbarManager("onActivityDestroyed: taskbar is null!", displayId); + } + mUnfoldProgressProvider.setSourceProvider(null); + } + }; + + UnfoldTransitionProgressProvider.TransitionProgressListener mUnfoldTransitionProgressListener = + new UnfoldTransitionProgressProvider.TransitionProgressListener() { + @Override + public void onTransitionStarted() { + debugPrimaryTaskbar("fold/unfold transition started getting called."); + } + + @Override + public void onTransitionProgress(float progress) { + debugPrimaryTaskbar( + "fold/unfold transition progress getting called. | progress=" + + progress); + } + + @Override + public void onTransitionFinishing() { + debugPrimaryTaskbar( + "fold/unfold transition finishing getting called."); + + } + + @Override + public void onTransitionFinished() { + debugPrimaryTaskbar( + "fold/unfold transition finished getting called."); + } + }; + + @SuppressLint("WrongConstant") + public TaskbarManagerImpl( + Context context, + AllAppsActionManager allAppsActionManager, + TaskbarNavButtonCallbacks navCallbacks, + PerDisplayRepository recentsWindowManagerRepository, + DisplaysWithDecorationsRepositoryCompat displaysWithDecorationsRepositoryCompat, + CoroutineDispatcher dispatcher) { + mBaseContext = context; + mPrimaryDisplayId = mBaseContext.getDisplayId(); + mAllAppsActionManager = allAppsActionManager; + mNavCallbacks = navCallbacks; + mRecentsWindowManagerRepository = recentsWindowManagerRepository; + mDisplaysWithDecorationsRepositoryCompat = displaysWithDecorationsRepositoryCompat; + + // Set up primary display. + debugPrimaryTaskbar("TaskbarManager constructor"); + mDisplayManager = mBaseContext.getSystemService(DisplayManager.class); + mPrimaryWindowContext = createWindowContext(mPrimaryDisplayId); + addWindowContextToMap(mPrimaryDisplayId, mPrimaryWindowContext); + mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class); + DesktopVisibilityController.INSTANCE.get( + mPrimaryWindowContext).registerTaskbarDesktopModeListener( + mTaskbarDesktopModeListener); + createTaskbarRootLayout(mPrimaryDisplayId); + createNavButtonController(mPrimaryDisplayId); + createAndRegisterComponentCallbacks(mPrimaryDisplayId); + + SettingsCache.INSTANCE.get(mPrimaryWindowContext) + .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener); + SettingsCache.INSTANCE.get(mPrimaryWindowContext) + .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener); + if (DesktopExperienceFlags.ENABLE_SYS_DECORS_CALLBACKS_VIA_WM.isTrue() + && DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) { + displaysWithDecorationsRepositoryCompat + .registerDisplayDecorationListener(this, dispatcher); + } else { + SystemDecorationChangeObserver.getINSTANCE().get(mPrimaryWindowContext) + .registerDisplayDecorationListener(this); + addSystemDecorationForDisplaysAtBoot(); + } + mShutdownReceiver = + new SimpleBroadcastReceiver( + mPrimaryWindowContext, UI_HELPER_EXECUTOR, i -> destroyAllTaskbars()); + + mShutdownReceiver.register(Intent.ACTION_SHUTDOWN); + if (enableGrowthNudge()) { + // TODO: b/397739323 - Add permission to limit access to Growth Framework. + mGrowthBroadcastReceiver = + new SimpleBroadcastReceiver( + mPrimaryWindowContext, UI_HELPER_EXECUTOR, this::showGrowthNudge); + mGrowthBroadcastReceiver.register(null, GROWTH_NUDGE_PERMISSION, RECEIVER_EXPORTED, + BROADCAST_SHOW_NUDGE); + } else { + mGrowthBroadcastReceiver = null; + } + + mIsAndroidPC = getPrimaryWindowContext().getPackageManager().hasSystemFeature(FEATURE_PC); + mSupportsFreeformWindowsManagement = getFreeformWindowsManagementInfo(); + + if (eligibleForPerceptibleTasks()) { + mTaskStackListener = new PerceptibleTaskListener(); + TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); + } else { + mTaskStackListener = null; + } + recreateTaskbarForDisplay(mPrimaryDisplayId, /* duration= */ 0); + + debugPrimaryTaskbar("TaskbarManager created"); + } + + /** + * Calls {@link #onDisplayAddSystemDecorations(int)} for all displays + * TODO b/408503553: Remove when WM is used instead of CommandQueue for system decorations. + */ + private void addSystemDecorationForDisplaysAtBoot() { + if (mDisplayManager == null) { + return; + } + + for (Display display : mDisplayManager.getDisplays()) { + onDisplayAddSystemDecorations(display.getDisplayId()); + } + } + + public LooperExecutor getPerWindowUiExecutor() { + return TASKBAR_UI_THREAD; + } + + private void handleDisplayUpdatesForPerceptibleTasks() { + // 1. When desktop mode changes, detect eligibility for perceptible tasks. + // 2. When no longer eligible for perceptible tasks, turn off and clean up. + mSupportsFreeformWindowsManagement = getFreeformWindowsManagementInfo(); + if (eligibleForPerceptibleTasks()) { + if (mTaskStackListener == null) { + mTaskStackListener = new PerceptibleTaskListener(); + TaskStackChangeListeners.getInstance() + .registerTaskStackListener(mTaskStackListener); + } + } else { + // not eligible for perceptible tasks, so we should unregister the listener + if (mTaskStackListener != null) { + mTaskStackListener.unregisterListener(); + mTaskStackListener = null; + } + } + } + + private boolean getFreeformWindowsManagementInfo() { + return getPrimaryWindowContext().getPackageManager().hasSystemFeature( + FEATURE_FREEFORM_WINDOW_MANAGEMENT); + } + + private void destroyAllTaskbars() { + debugPrimaryTaskbar("destroyAllTaskbars"); + for (Entry entry : new ArraySet<>(mTaskbars.entrySet())) { + int displayId = entry.getKey(); + debugTaskbarManager("destroyAllTaskbars: call destroyTaskbarForDisplay", displayId); + destroyTaskbarForDisplay(entry.getValue()); + + debugTaskbarManager("destroyAllTaskbars: call removeTaskbarRootViewFromWindow", + displayId); + removeTaskbarRootViewFromWindow(displayId); + } + } + + private void destroyTaskbarForDisplay(int displayId) { + TaskbarActivityContext taskbar = mTaskbars.get(displayId); + if (taskbar == null) { + debugTaskbarManager("destroyTaskbarForDisplay: taskbar is NULL!", displayId); + return; + } + destroyTaskbarForDisplay(taskbar); + } + + private void destroyTaskbarForDisplay(TaskbarActivityContext taskbar) { + final int displayId = taskbar.getDisplayId(); + debugTaskbarManager("destroyTaskbarForDisplay", displayId); + taskbar.onDestroy(); + // remove all defaults that we store + removeTaskbarFromMap(displayId); + + DeviceProfile dp = getDeviceProfile(displayId); + if (dp == null || !isTaskbarEnabled(dp)) { + removeTaskbarRootViewFromWindow(displayId); + } + } + + /** + * Show Taskbar upon receiving broadcast + */ + private void showTaskbarFromBroadcast(Intent intent, int displayId) { + debugTaskbarManager("destroyTaskbarForDisplay", displayId); + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && taskbar != null) { + taskbar.showTaskbarFromBroadcast(); + } + } + + private void showGrowthNudge(Intent intent) { + if (!enableGrowthNudge()) { + return; + } + if (BROADCAST_SHOW_NUDGE.equals(intent.getAction())) { + // TODO: b/397738606 - extract the details and create a nudge payload. + Log.d(GROWTH_FRAMEWORK_TAG, "Intent received"); + } + } + + /** + * Shows or hides the All Apps view in the Taskbar or Launcher, based on its current + * visibility on the System UI tracked focused display. + */ + private void toggleAllAppsSearch() { + TaskbarActivityContext taskbar = getTaskbarForDisplay(getFocusedDisplayId()); + if (taskbar == null) { + // Home All Apps should be toggled from this class, because the controllers are not + // initialized when Taskbar is disabled (i.e. TaskbarActivityContext is null). + if (mActivity instanceof Launcher l) l.toggleAllApps(true); + } else { + taskbar.getControllers().uiController.toggleAllApps(true); + } + } + + /** + * Displays a frame of the first Launcher reveal animation. + * + * This should be used to run a first Launcher reveal animation whose progress matches a swipe + * progress. + */ + public AnimatorPlaybackController createLauncherStartFromSuwAnim(int duration) { + TaskbarActivityContext taskbar = getTaskbarForDisplay(mPrimaryDisplayId); + return taskbar == null ? null : taskbar.createLauncherStartFromSuwAnim(duration); + } + + /** Called when the user is unlocked */ + public void onUserUnlocked() { + debugPrimaryTaskbar("onUserUnlocked"); + mUserUnlocked = true; + addRecreationListener(mPrimaryDisplayId); + debugPrimaryTaskbar("onUserUnlocked: recreating all taskbars!"); + // Create DPs for all connected displays if required. + for (int i = 0; i < mWindowContexts.size(); i++) { + int displayId = mWindowContexts.keyAt(i); + if (displayId != mPrimaryDisplayId && !mExternalDeviceProfiles.contains(displayId)) { + createExternalDeviceProfile(displayId); + addRecreationListener(displayId); + } + } + + recreateTaskbars(); + for (Entry entry : mTaskbars.entrySet()) { + int displayId = entry.getKey(); + debugTaskbarManager("onUserUnlocked: addTaskbarRootViewToWindow()", displayId); + addTaskbarRootViewToWindow(entry.getValue()); + } + } + + /** + * Sets a {@link StatefulActivity} to act as taskbar callback + */ + public void setActivity(@NonNull StatefulActivity activity) { + debugPrimaryTaskbar("setActivity: mActivity=" + mActivity); + if (mActivity == activity) { + debugPrimaryTaskbar("setActivity: No need to set activity!"); + return; + } + removeActivityCallbacksAndListeners(); + mActivity = activity; + mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged); + debugPrimaryTaskbar("setActivity: registering activity lifecycle callbacks."); + mActivity.addEventCallback(EVENT_DESTROYED, mActivityOnDestroyCallback); + UnfoldTransitionProgressProvider unfoldTransitionProgressProvider = + getUnfoldTransitionProgressProviderForActivity(activity); + if (unfoldTransitionProgressProvider != null) { + unfoldTransitionProgressProvider.addCallback(mUnfoldTransitionProgressListener); + } + mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider); + + if (activity instanceof RecentsViewContainer recentsViewContainer) { + setRecentsViewContainer(recentsViewContainer); + } + } + + /** + * Sets the current RecentsViewContainer, from which we create a TaskbarUIController. + */ + public void setRecentsViewContainer(@NonNull RecentsViewContainer recentsViewContainer) { + debugPrimaryTaskbar("setRecentsViewContainer"); + if (mRecentsViewContainer == recentsViewContainer) { + return; + } + if (mRecentsViewContainer == mActivity) { + // When switching to RecentsWindowManager (not an Activity), the old mActivity is not + // destroyed, nor is there a new Activity to replace it. Thus if we don't clear it here, + // it will not get re-set properly if we return to the Activity (e.g. NexusLauncher). + mActivityOnDestroyCallback.run(); + } + mRecentsViewContainer = recentsViewContainer; + TaskbarActivityContext taskbar = getCurrentActivityContext(); + if (taskbar != null) { + taskbar.setUIController( + createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer, + mPrimaryDisplayId)); + } + } + + /** + * Returns an {@link UnfoldTransitionProgressProvider} to use while the given StatefulActivity + * is active. + */ + private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity( + StatefulActivity activity) { + debugPrimaryTaskbar("getUnfoldTransitionProgressProviderForActivity"); + if (!enableUnfoldStateAnimation()) { + if (activity instanceof QuickstepLauncher ql) { + return ql.getUnfoldTransitionProgressProvider(); + } + } else { + return SystemUiProxy.INSTANCE.get(mBaseContext).getUnfoldTransitionProvider(); + } + return null; + } + + /** Creates a {@link TaskbarUIController} to use with non default displays. */ + private TaskbarUIController createTaskbarUIControllerForNonDefaultDisplay(int displayId) { + debugTaskbarManager("createTaskbarUIControllerForNonDefaultDisplay", displayId); + BaseContainerInterface containerInterface = OverviewComponentObserver.INSTANCE.get( + mBaseContext).getContainerInterface(displayId); + if (containerInterface != null) { + RecentsViewContainer container = containerInterface.getCreatedContainer(); + if (container instanceof RecentsWindowManager) { + return createTaskbarUIControllerForRecentsViewContainer(container, displayId); + } + } + return new TaskbarUIController(); + } + + /** + * Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active. + */ + private TaskbarUIController createTaskbarUIControllerForRecentsViewContainer( + RecentsViewContainer container, int displayId) { + debugTaskbarManager("createTaskbarUIControllerForRecentsViewContainer", displayId); + if (!isExternalDisplay(displayId) + && mActivity instanceof QuickstepLauncher quickstepLauncher) { + // If 1P Launcher is default, always use LauncherTaskbarUIController, regardless of + // whether the recents container is NexusLauncherActivity or RecentsWindowManager. This + // is only applicable for primary displays. In case of foldables both displays have + // primary display ID and only one of them is primary at a given time, the other one is + // inactive or has limited functionality (has different display ID in that case). + return new LauncherTaskbarUIController(quickstepLauncher); + } + // If a 3P Launcher is default, always use FallbackTaskbarUIController regardless of + // whether the recents container is RecentsActivity or RecentsWindowManager. + if (container instanceof RecentsActivity recentsActivity) { + return new FallbackTaskbarUIController<>(recentsActivity); + } + if (container instanceof RecentsWindowManager recentsWindowManager) { + return new FallbackTaskbarUIController<>(recentsWindowManager); + } + return new TaskbarUIController(); + } + + /** + * This method is called multiple times (ex. initial init, then when user unlocks) in which case + * we fully want to destroy existing taskbars and create all desired new ones. + * In other case (folding/unfolding) we don't need to remove and add window. + */ + public synchronized void recreateTaskbars() { + for (int i = 0; i < mWindowContexts.size(); i++) { + int displayId = mWindowContexts.keyAt(i); + debugTaskbarManager("recreateTaskbars", displayId); + recreateTaskbarForDisplay(displayId, 0); + } + } + + /** + * This method is called multiple times (ex. initial init, then when user unlocks) in which case + * we fully want to destroy an existing taskbar for a specified display and create a new one. + * In other case (folding/unfolding) we don't need to remove and add window. + */ + @VisibleForTesting + protected void recreateTaskbarForDisplay(int displayId, int duration) { + debugTaskbarManager("recreateTaskbarForDisplay: ", displayId); + Trace.beginSection("recreateTaskbarForDisplay"); + try { + debugTaskbarManager("recreateTaskbarForDisplay: getting device profile", displayId); + // TODO (b/381113004): make this display-specific via getWindowContext() + DeviceProfile dp = getDeviceProfile(displayId); + + // All Apps action is unrelated to navbar unification, so we only need to check DP. + final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent; + mAllAppsActionManager.setTaskbarPresent(isLargeScreenTaskbar); + debugTaskbarManager("recreateTaskbarForDisplay: destroying taskbar", displayId); + destroyTaskbarForDisplay(displayId); + + boolean displayExists = getDisplay(displayId) != null; + boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp); + debugTaskbarManager("recreateTaskbarForDisplay: isTaskbarEnabled=" + isTaskbarEnabled + + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null) + + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION + + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent) + + " displayExists=" + displayExists, displayId); + if (!isTaskbarEnabled || !isLargeScreenTaskbar || !displayExists) { + SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(mBaseContext); + systemUiProxy.notifyTaskbarStatus(/* visible */ false, /* stashed */ false); + systemUiProxy.setHasBubbleBar(false); + if (!isTaskbarEnabled || !displayExists) { + debugTaskbarManager( + "recreateTaskbarForDisplay: exiting bc (!isTaskbarEnabled || " + + "!displayExists)", + displayId); + return; + } + } + + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (enableTaskbarNoRecreate() || taskbar == null) { + debugTaskbarManager("recreateTaskbarForDisplay: creating taskbar", displayId); + taskbar = createTaskbarActivityContext(dp, displayId); + if (taskbar == null) { + debugTaskbarManager( + "recreateTaskbarForDisplay: new taskbar instance is null!", displayId); + return; + } + } else { + debugTaskbarManager("recreateTaskbarForDisplay: updating taskbar device profile", + displayId); + taskbar.updateDeviceProfile(dp); + } + TaskbarSharedState sharedState = getSharedStateForDisplay(displayId); + sharedState.startTaskbarVariantIsTransient = taskbar.isTransientTaskbar(); + sharedState.allAppsVisible = sharedState.allAppsVisible && isLargeScreenTaskbar; + taskbar.init(sharedState, duration); + + // Non default displays should not use LauncherTaskbarUIController as they shouldn't + // have access to the Launcher activity. + if (isExternalDisplay(displayId)) { + taskbar.setUIController(createTaskbarUIControllerForNonDefaultDisplay(displayId)); + } else if (mRecentsViewContainer != null) { + taskbar.setUIController( + createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer, + mPrimaryDisplayId)); + } + + if (enableTaskbarNoRecreate()) { + debugTaskbarManager("recreateTaskbarForDisplay: adding rootView", displayId); + addTaskbarRootViewToWindow(taskbar); + FrameLayout taskbarRootLayout = getTaskbarRootLayoutForDisplay(displayId); + if (taskbarRootLayout != null) { + debugTaskbarManager("recreateTaskbarForDisplay: adding root layout", displayId); + taskbarRootLayout.removeAllViews(); + taskbarRootLayout.addView(taskbar.getDragLayer()); + taskbar.notifyUpdateLayoutParams(); + } else { + debugTaskbarManager("recreateTaskbarForDisplay: taskbarRootLayout is null!", + displayId); + } + } + } finally { + Trace.endSection(); + } + } + + /** Called when the SysUI flags for a given display change. */ + public void onSystemUiFlagsChanged(@SystemUiStateFlags long systemUiStateFlags, int displayId) { + TaskbarSharedState sharedState = getSharedStateForDisplay(displayId); + if (DEBUG) { + Log.d(TAG, "SysUI flags changed: " + formatFlagChange(systemUiStateFlags, + sharedState.sysuiStateFlags, QuickStepContract::getSystemUiStateString)); + } + sharedState.sysuiStateFlags = systemUiStateFlags; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.updateSysuiStateFlags(systemUiStateFlags, false /* fromInit */); + } + } + + public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) { + if (mPrimaryNavButtonController != null) { + mPrimaryNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled); + } + } + + /** + * Sets the flag indicating setup UI is visible + */ + public void setSetupUIVisible(boolean isVisible) { + mAllAppsActionManager.setSetupUiVisible(isVisible); + for (int i = 0; i < mWindowContexts.size(); i++) { + int displayId = mWindowContexts.keyAt(i); + getSharedStateForDisplay(displayId).setupUIVisible = isVisible; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.setSetupUIVisible(isVisible); + } + } + } + + /** + * Sets wallpaper visibility for specific display. + */ + public void setWallpaperVisible(int displayId, boolean isVisible) { + getSharedStateForDisplay(displayId).wallpaperVisible = isVisible; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.setWallpaperVisible(isVisible); + } + } + + public void checkNavBarModes(int displayId) { + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.checkNavBarModes(); + } + } + + public void finishBarAnimations(int displayId) { + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.finishBarAnimations(); + } + } + + public void touchAutoDim(int displayId, boolean reset) { + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.touchAutoDim(reset); + } + } + + public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode, + boolean animate) { + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.transitionTo(barMode, animate); + } + } + + public void appTransitionPending(boolean pending) { + TaskbarActivityContext taskbar = getTaskbarForDisplay(mPrimaryDisplayId); + if (taskbar != null) { + taskbar.appTransitionPending(pending); + } + } + + private boolean isTaskbarEnabled(DeviceProfile deviceProfile) { + return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent; + } + + public void onRotationProposal(int rotation, boolean isValid) { + TaskbarActivityContext taskbar = getTaskbarForDisplay(mPrimaryDisplayId); + if (taskbar != null) { + taskbar.onRotationProposal(rotation, isValid); + } + } + + public void disableNavBarElements(int displayId, int state1, int state2, boolean animate) { + TaskbarSharedState sharedState = getSharedStateForDisplay(displayId); + sharedState.disableNavBarDisplayId = displayId; + sharedState.disableNavBarState1 = state1; + sharedState.disableNavBarState2 = state2; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.disableNavBarElements(displayId, state1, state2, animate); + } + } + + public void onSystemBarAttributesChanged(int displayId, int behavior) { + TaskbarSharedState sharedState = getSharedStateForDisplay(displayId); + sharedState.systemBarAttrsDisplayId = displayId; + sharedState.systemBarAttrsBehavior = behavior; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.onSystemBarAttributesChanged(displayId, behavior); + } + } + + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + for (int i = 0; i < mWindowContexts.size(); i++) { + int displayId = mWindowContexts.keyAt(i); + getSharedStateForDisplay(displayId).barMode = barMode; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.onTransitionModeUpdated(barMode, checkBarModes); + } + } + } + + public void onNavButtonsDarkIntensityChanged(float darkIntensity) { + for (int i = 0; i < mWindowContexts.size(); i++) { + int displayId = mWindowContexts.keyAt(i); + getSharedStateForDisplay(displayId).navButtonsDarkIntensity = darkIntensity; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.onNavButtonsDarkIntensityChanged(darkIntensity); + } + } + } + + public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { + TaskbarSharedState sharedState = getSharedStateForDisplay(displayId); + sharedState.mLumaSamplingDisplayId = displayId; + sharedState.mIsLumaSamplingEnabled = enable; + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null) { + taskbar.onNavigationBarLumaSamplingEnabled(displayId, enable); + } + } + + /** + * Signal from SysUI indicating that a non-mirroring display was just connected to the + * primary device or a previously mirroring display is switched to extended mode. + */ + @Override + public void onDisplayAddSystemDecorations(int displayId) { + debugTaskbarManager("onDisplayAddSystemDecorations: ", displayId); + Display display = getDisplay(displayId); + if (display == null) { + debugTaskbarManager("onDisplayAddSystemDecorations: can't find display!", displayId); + return; + } + + if (!isExternalDisplay(displayId)) { + debugTaskbarManager( + "onDisplayAddSystemDecorations: not an external display! | " + + "isExternalDisplay=" + isExternalDisplay(displayId), displayId); + return; + } + debugTaskbarManager("onDisplayAddSystemDecorations: creating new windowContext!", + displayId); + Context newWindowContext = createWindowContext(displayId); + if (newWindowContext != null) { + debugTaskbarManager("onDisplayAddSystemDecorations: add new windowContext to map!", + displayId); + WindowManager wm = newWindowContext.getSystemService(WindowManager.class); + if (wm == null || !wm.shouldShowSystemDecors(displayId)) { + String wmStatus = wm == null ? "WindowManager is null!" : "WindowManager exists"; + boolean showDecor = wm != null && wm.shouldShowSystemDecors(displayId); + debugTaskbarManager( + "onDisplayAddSystemDecorations:\n\t" + wmStatus + "\n\tshowSystemDecors=" + + showDecor, displayId); + return; + } + addWindowContextToMap(displayId, newWindowContext); + debugTaskbarManager("onDisplayAddSystemDecorations: creating RootLayout!", displayId); + + createExternalDeviceProfile(displayId); + + debugTaskbarManager("onDisplayAddSystemDecorations: creating RootLayout!", displayId); + createTaskbarRootLayout(displayId); + + debugTaskbarManager("onDisplayAddSystemDecorations: creating NavButtonController!", + displayId); + createNavButtonController(displayId); + + debugTaskbarManager( + "onDisplayAddSystemDecorations: createAndRegisterComponentCallbacks!", + displayId); + createAndRegisterComponentCallbacks(displayId); + + debugTaskbarManager( + "onDisplayAddSystemDecorations: addRecreationListener!", displayId); + addRecreationListener(displayId); + + debugTaskbarManager("onDisplayAddSystemDecorations: recreateTaskbarForDisplay!", + displayId); + recreateTaskbarForDisplay(displayId, 0); + } else { + debugTaskbarManager("onDisplayAddSystemDecorations: newWindowContext is NULL!", + displayId); + } + + debugTaskbarManager("onDisplayAddSystemDecorations: finished!", displayId); + } + + /** + * Signal from SysUI indicating that a previously connected non-mirroring display was just + * removed from the primary device. + */ + @Override + public void onDisplayRemoved(int displayId) { + debugTaskbarManager("onDisplayRemoved: ", displayId); + if (!isExternalDisplay(displayId)) { + debugTaskbarManager( + "onDisplayRemoved: not an external display! | " + + "isExternalDisplay=" + isExternalDisplay(displayId), displayId); + return; + } + + Context windowContext = getWindowContext(displayId); + if (windowContext != null) { + debugTaskbarManager("onDisplayRemoved: removing NavButtonController!", displayId); + removeNavButtonController(displayId); + + debugTaskbarManager("onDisplayRemoved: removeAndUnregisterComponentCallbacks!", + displayId); + removeAndUnregisterComponentCallbacks(displayId); + + debugTaskbarManager("onDisplayRemoved: removeRecreationListener!", displayId); + removeRecreationListener(displayId); + + debugTaskbarManager("onDisplayRemoved: removing DeviceProfile from map!", displayId); + removeDeviceProfileFromMap(displayId); + + debugTaskbarManager("onDisplayRemoved: destroying Taskbar!", displayId); + destroyTaskbarForDisplay(displayId); + + debugTaskbarManager("onDisplayRemoved: removing WindowContext from map!", displayId); + removeWindowContextFromMap(displayId); + + debugTaskbarManager("onDisplayRemoved: destroying SharedState from map!", displayId); + destroySharedStateForDisplay(displayId); + + debugTaskbarManager("onDisplayRemoved: finished!", displayId); + } else { + debugTaskbarManager("onDisplayRemoved: windowContext is null!", displayId); + } + } + + /** + * Signal from SysUI indicating that system decorations should be removed from the display. + */ + @Override + public void onDisplayRemoveSystemDecorations(int displayId) { + // The display mirroring starts. The handling logic is the same as when removing a + // display. + onDisplayRemoved(displayId); + } + + private void removeActivityCallbacksAndListeners() { + if (mActivity != null) { + mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged); + debugPrimaryTaskbar("unregistering activity lifecycle callbacks"); + mActivity.removeEventCallback(EVENT_DESTROYED, mActivityOnDestroyCallback); + UnfoldTransitionProgressProvider unfoldTransitionProgressProvider = + getUnfoldTransitionProgressProviderForActivity(mActivity); + if (unfoldTransitionProgressProvider != null) { + unfoldTransitionProgressProvider.removeCallback(mUnfoldTransitionProgressListener); + } + } + } + + /** + * Called when the manager is no longer needed + */ + public void destroy() { + debugPrimaryTaskbar("TaskbarManager#destroy()"); + mRecentsViewContainer = null; + debugPrimaryTaskbar("destroy: removing activity callbacks"); + DesktopVisibilityController.INSTANCE.get( + mPrimaryWindowContext).unregisterTaskbarDesktopModeListener( + mTaskbarDesktopModeListener); + removeActivityCallbacksAndListeners(); + destroySharedStateForAllDisplays(); + if (mGrowthBroadcastReceiver != null) { + mGrowthBroadcastReceiver.unregisterReceiverSafely(); + } + + removeRecreationListener(mPrimaryDisplayId); + + if (LawnchairApp.isRecentsEnabled()) { + SettingsCache.INSTANCE.get(mPrimaryWindowContext) + .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener); + SettingsCache.INSTANCE.get(mPrimaryWindowContext) + .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener); + } + // Lawnchair-TODO: DesktopExperienceFlags.ENABLE_SYS_DECORS_CALLBACKS_VIA_WM.isTrue() + // && DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue() + if (false) { + mDisplaysWithDecorationsRepositoryCompat.unregisterDisplayDecorationListener(this); + } else { + SystemDecorationChangeObserver.getINSTANCE().get(mPrimaryWindowContext) + .unregisterDisplayDecorationListener(this); + } + debugPrimaryTaskbar("destroy: unregistering component callbacks"); + removeAndUnregisterComponentCallbacks(mPrimaryDisplayId); + mShutdownReceiver.unregisterReceiverSafely(); + if (mTaskStackListener != null) { + mTaskStackListener.unregisterListener(); + } + + debugPrimaryTaskbar("destroy: destroying all taskbars!"); + removeWindowContextFromMap(mPrimaryDisplayId); + destroyAllTaskbars(); + debugPrimaryTaskbar("destroy: finished!"); + } + + private boolean eligibleForPerceptibleTasks() { + // Perceptible tasks feature (oom boosting) is eligible for android PC devices, and + // other android devices that supports free form windows + // + // - isAndroidPC is set per device (in this case, desktop devices) + // - supportsFreeformWindowsManagement is dynamic, and is to be used for the use-case where + // user plugs in their device to external displays + // Lawnchair-TODO: AM Flags, perceptibleTasks + //return Flags.perceptibleTasks() + // && (mIsAndroidPC || mSupportsFreeformWindowsManagement); + return false; + } + + public @Nullable TaskbarActivityContext getCurrentActivityContext() { + return getTaskbarForDisplay(mPrimaryDisplayId); + } + + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarManager:"); + // iterate through taskbars and do the dump for each + for (Entry entry : mTaskbars.entrySet()) { + int displayId = entry.getKey(); + TaskbarActivityContext taskbar = entry.getValue(); + pw.println(prefix + "\tTaskbar at display " + displayId + ":"); + if (taskbar == null) { + pw.println(prefix + "\t\tTaskbarActivityContext: null"); + } else { + taskbar.dumpLogs(prefix + "\t\t", pw); + } + } + } + + private void addTaskbarRootViewToWindow(@NonNull TaskbarActivityContext taskbar) { + int displayId = taskbar.getDisplayId(); + debugTaskbarManager("addTaskbarRootViewToWindow:", displayId); + if (!enableTaskbarNoRecreate()) { + debugTaskbarManager("addTaskbarRootViewToWindow: taskbar null", displayId); + return; + } + + if (getDisplay(displayId) == null) { + debugTaskbarManager("addTaskbarRootViewToWindow: display null", displayId); + return; + } + + if (!isTaskbarRootLayoutAddedForDisplay(displayId)) { + FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId); + WindowManager windowManager = getWindowManager(displayId); + if (rootLayout != null && windowManager != null) { + windowManager.addView(rootLayout, taskbar.getWindowLayoutParams()); + mAddedRootLayouts.put(displayId, true); + } else { + String rootLayoutStatus = + (rootLayout == null) ? "rootLayout is NULL!" : "rootLayout exists!"; + String wmStatus = (windowManager == null) ? "windowManager is NULL!" + : "windowManager exists!"; + debugTaskbarManager( + "addTaskbarRootViewToWindow: \n\t" + rootLayoutStatus + "\n\t" + wmStatus, + displayId); + } + } else { + debugTaskbarManager("addTaskbarRootViewToWindow: rootLayout already added!", displayId); + } + } + + private void removeTaskbarRootViewFromWindow(int displayId) { + debugTaskbarManager("removeTaskbarRootViewFromWindow", displayId); + FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId); + if (!enableTaskbarNoRecreate() || rootLayout == null) { + return; + } + + WindowManager windowManager = getWindowManager(displayId); + if (isTaskbarRootLayoutAddedForDisplay(displayId) && windowManager != null) { + windowManager.removeViewImmediate(rootLayout); + mAddedRootLayouts.put(displayId, false); + removeTaskbarRootLayoutFromMap(displayId); + } else { + debugTaskbarManager("removeTaskbarRootViewFromWindow: WindowManager is null", + displayId); + } + } + + /** + * Returns the {@link TaskbarUIController} associated with the given display ID. + * TODO(b/395061396): Remove this method when overview in widow is enabled. + * + * @param displayId The ID of the display to retrieve the taskbar for. + * @return The {@link TaskbarUIController} for the specified display, or + * {@code null} if no taskbar is associated with that display. + */ + @Nullable + public TaskbarUIController getUIControllerForDisplay(int displayId) { + TaskbarActivityContext taskbarActivityContext = getTaskbarForDisplay(displayId); + if (taskbarActivityContext == null) { + return null; + } + + return taskbarActivityContext.getControllers().uiController; + } + + /** + * Retrieves whether RootLayout was added to window for specific display, or false if no + * such mapping has been made. + * + * @param displayId The ID of the display for which to retrieve the taskbar root layout. + * @return if RootLayout was added to window {@link Boolean} for a display or {@code false}. + */ + private boolean isTaskbarRootLayoutAddedForDisplay(int displayId) { + return mAddedRootLayouts.get(displayId); + } + + /** + * Returns the {@link TaskbarActivityContext} associated with the given display ID. + * + * @param displayId The ID of the display to retrieve the taskbar for. + * @return The {@link TaskbarActivityContext} for the specified display, or + * {@code null} if no taskbar is associated with that display. + */ + public TaskbarActivityContext getTaskbarForDisplay(int displayId) { + return mTaskbars.get(displayId); + } + + private TaskbarSharedState getSharedStateForDisplay(int displayId) { + TaskbarSharedState sharedState = mTaskbarSharedStates.getOrDefault(displayId, + new TaskbarSharedState()); + mTaskbarSharedStates.put(displayId, sharedState); + + // Verify if shared state is properly initialised. Sometimes it can only be initialised with + // subsequent access. For example, onSystemUiFlagsChanged gets called before + // recreateTaskbarForDisplay when display is added resulting into windowContext being null. + if (sharedState.taskbarSystemActionPendingIntent == null && mWindowContexts.contains( + displayId)) { + debugTaskbarManager("getSharedStateForDisplay: initialising shared state", displayId); + + Context windowContext = mWindowContexts.get(displayId); + SimpleBroadcastReceiver broadcastReceiver = new SimpleBroadcastReceiver(windowContext, + UI_HELPER_EXECUTOR, (intent) -> showTaskbarFromBroadcast(intent, displayId)); + mTaskbarBroadcastReceivers.put(displayId, broadcastReceiver); + + sharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(windowContext, + SYSTEM_ACTION_ID_TASKBAR, + new Intent(ACTION_SHOW_TASKBAR).setPackage(windowContext.getPackageName()), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + UI_HELPER_EXECUTOR.execute( + () -> broadcastReceiver.register(RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR)); + } + + return sharedState; + } + + /** Should only be called when the TaskbarManager class is being destroyed. */ + private void destroySharedStateForAllDisplays() { + debugPrimaryTaskbar("getSharedStateForDisplay: destroying all shared state", + /* verbose= */ false); + + for (SimpleBroadcastReceiver broadcastReceiver : mTaskbarBroadcastReceivers.values()) { + broadcastReceiver.unregisterReceiverSafely(); + } + + mTaskbarBroadcastReceivers.clear(); + mTaskbarSharedStates.clear(); + } + + /** Should be called when the taskbar is not going to be recreated, example display removed. */ + private void destroySharedStateForDisplay(int displayId) { + debugTaskbarManager("getSharedStateForDisplay: destroying shared state", displayId); + SimpleBroadcastReceiver broadcastReceiver = mTaskbarBroadcastReceivers.remove(displayId); + if (broadcastReceiver != null) { + broadcastReceiver.unregisterReceiverSafely(); + } + + mTaskbarSharedStates.remove(displayId); + } + + /** + * Creates a {@link TaskbarActivityContext} for the given display and adds it to the map. + * + * @param dp The {@link deviceprofile} for the display. + * @param displayId The ID of the display. + */ + private @Nullable TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, + int displayId) { + Display display = getDisplay(displayId); + if (display == null) { + debugTaskbarManager("createTaskbarActivityContext: display null", displayId); + return null; + } + + Context navigationBarPanelContext = null; + if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { + navigationBarPanelContext = mBaseContext.createWindowContext(display, + TYPE_NAVIGATION_BAR_PANEL, null); + } + + TaskbarActivityContext newTaskbar = new TaskbarActivityContext(displayId, + getWindowContext(displayId), navigationBarPanelContext, dp, + getNavButtonController(displayId), mUnfoldProgressProvider, + !isExternalDisplay(displayId), getPrimaryDisplayId(), + SystemUiProxy.INSTANCE.get(mBaseContext)); + + addTaskbarToMap(displayId, newTaskbar); + return newTaskbar; + } + + /** + * Creates a {@link deviceprofile} for the given display and adds it to the map. + * + * @param displayId The ID of the display. + */ + private void createExternalDeviceProfile(int displayId) { + if (!mUserUnlocked || displayId == mPrimaryDisplayId) { + return; + } + + InvariantDeviceProfile idp = LauncherAppState.getIDP(mPrimaryWindowContext); + if (idp == null) { + return; + } + + Context displayContext = getWindowContext(displayId); + if (displayContext == null) { + return; + } + + DeviceProfile externalDeviceProfile = idp.createDeviceProfileForSecondaryDisplay( + displayContext); + mExternalDeviceProfiles.put(displayId, externalDeviceProfile); + } + + /** + * Gets a {@link deviceprofile} for the given displayId. + * + * @param displayId The ID of the display. + */ + private @Nullable DeviceProfile getDeviceProfile(int displayId) { + if (!mUserUnlocked) { + return null; + } + + InvariantDeviceProfile idp = LauncherAppState.getIDP(mPrimaryWindowContext); + if (idp == null) { + return null; + } + + if (!isExternalDisplay(displayId)) { + return idp.getDeviceProfile(mPrimaryWindowContext); + } + + return mExternalDeviceProfiles.get(displayId); + } + + /** + * Removes the {@link deviceprofile} associated with the given display ID from the map. + * + * @param displayId The ID of the display for which to remove the taskbar. + */ + private void removeDeviceProfileFromMap(int displayId) { + mExternalDeviceProfiles.delete(displayId); + } + + private void addRecreationListener(int displayId) { + if (!mUserUnlocked) { + return; + } + + DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListenerForDisplay( + mRecreationListener, displayId); + } + + private void removeRecreationListener(int displayId) { + if (!mUserUnlocked) { + return; + } + + DisplayController.INSTANCE.get(mPrimaryWindowContext).removeChangeListenerForDisplay( + mRecreationListener, displayId); + } + + /** + * Create {@link ComponentCallbacks} for the given display and register it to the relevant + * WindowContext. For external displays, populate maps. + * + * @param displayId The ID of the display. + */ + private void createAndRegisterComponentCallbacks(int displayId) { + debugTaskbarManager("createAndRegisterComponentCallbacks", displayId); + ComponentCallbacks callbacks = new ComponentCallbacks() { + private Configuration mOldConfig = + getWindowContext(displayId).getResources().getConfiguration(); + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager", + "onConfigurationChanged: " + newConfig); + debugTaskbarManager("onConfigurationChanged: " + newConfig, displayId); + + DeviceProfile dp = getDeviceProfile(displayId); + int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES; + + if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) { + debugTaskbarManager("onConfigurationChanged: theme changed", displayId); + // Only recreate for theme changes, not other UI mode changes such as docking. + int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK); + int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK); + if (oldUiNightMode == newUiNightMode) { + configDiff &= ~ActivityInfo.CONFIG_UI_MODE; + } + } + + debugTaskbarManager("onConfigurationChanged: | configDiff=" + + Configuration.configurationDiffToString(configDiff), displayId); + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (configDiff != 0 || taskbar == null) { + debugTaskbarManager("onConfigurationChanged: call recreateTaskbars", displayId); + recreateTaskbarForDisplay(displayId, /* duration= */ 0); + } else if (dp != null) { + // Config change might be handled without re-creating the taskbar + if (!isTaskbarEnabled(dp)) { + debugPrimaryTaskbar( + "onConfigurationChanged: isTaskbarEnabled(dp)=False | " + + "destroyTaskbarForDisplay"); + destroyTaskbarForDisplay(displayId); + } else { + debugPrimaryTaskbar("onConfigurationChanged: isTaskbarEnabled(dp)=True"); + if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { + // Re-initialize for screen size change? Should this be done + // by looking at screen-size change flag in configDiff in the + // block above? + debugPrimaryTaskbar("onConfigurationChanged: call recreateTaskbars"); + recreateTaskbarForDisplay(displayId, /* duration= */ 0); + } else { + debugPrimaryTaskbar( + "onConfigurationChanged: updateDeviceProfile for current " + + "taskbar."); + taskbar.updateDeviceProfile(dp); + } + } + } else { + taskbar.onConfigurationChanged(configDiff); + } + mOldConfig = new Configuration(newConfig); + // reset taskbar was pinned value, so we don't automatically unstash taskbar upon + // user unfolding the device. + getSharedStateForDisplay(displayId).setTaskbarWasPinned(false); + } + + @Override + public void onLowMemory() { + } + }; + if (!isExternalDisplay(displayId)) { + mPrimaryComponentCallbacks = callbacks; + mPrimaryWindowContext.registerComponentCallbacks(callbacks); + } else { + mComponentCallbacks.put(displayId, callbacks); + getWindowContext(displayId).registerComponentCallbacks(callbacks); + } + } + + /** + * Unregister {@link ComponentCallbacks} for the given display from its WindowContext. For + * external displays, remove from the map. + * + * @param displayId The ID of the display. + */ + private void removeAndUnregisterComponentCallbacks(int displayId) { + if (!isExternalDisplay(displayId)) { + mPrimaryWindowContext.unregisterComponentCallbacks(mPrimaryComponentCallbacks); + } else { + ComponentCallbacks callbacks = mComponentCallbacks.get(displayId); + getWindowContext(displayId).unregisterComponentCallbacks(callbacks); + mComponentCallbacks.delete(displayId); + } + } + + /** + * Creates a {@link TaskbarNavButtonController} for the given display and adds it to the map + * if it doesn't already exist. + * + * @param displayId The ID of the display + */ + private void createNavButtonController(int displayId) { + if (!isExternalDisplay(displayId)) { + mPrimaryNavButtonController = new TaskbarNavButtonController( + mPrimaryWindowContext, + mNavCallbacks, + SystemUiProxy.INSTANCE.get(mBaseContext), + new Handler(), + new ContextualSearchInvoker(mBaseContext)); + } else { + TaskbarNavButtonController navButtonController = new TaskbarNavButtonController( + getWindowContext(displayId), + mNavCallbacks, + SystemUiProxy.INSTANCE.get(mBaseContext), + new Handler(), + new ContextualSearchInvoker(mBaseContext)); + mNavButtonControllers.put(displayId, navButtonController); + } + } + + private TaskbarNavButtonController getNavButtonController(int displayId) { + return (!isExternalDisplay(displayId)) ? mPrimaryNavButtonController + : mNavButtonControllers.get(displayId); + } + + private void removeNavButtonController(int displayId) { + if (!isExternalDisplay(displayId)) { + mPrimaryNavButtonController = null; + } else { + mNavButtonControllers.delete(displayId); + } + } + + /** + * Adds the {@link TaskbarActivityContext} associated with the given display ID to taskbar + * map if there is not already a taskbar mapped to that displayId. + * + * @param displayId The ID of the display to retrieve the taskbar for. + * @param newTaskbar The new {@link TaskbarActivityContext} to add to the map. + */ + private void addTaskbarToMap(int displayId, TaskbarActivityContext newTaskbar) { + mTaskbars.putIfAbsent(displayId, newTaskbar); + } + + /** + * Removes the taskbar associated with the given display ID from the taskbar map. + * + * @param displayId The ID of the display for which to remove the taskbar. + */ + private void removeTaskbarFromMap(int displayId) { + mTaskbars.remove(displayId); + } + + /** + * Creates {@link FrameLayout} for the taskbar on the specified display and adds it to map. + * + * @param displayId The ID of the display for which to create the taskbar root layout. + */ + private void createTaskbarRootLayout(int displayId) { + debugTaskbarManager("createTaskbarRootLayout: ", displayId); + if (!enableTaskbarNoRecreate()) { + return; + } + + FrameLayout newTaskbarRootLayout = new FrameLayout(getWindowContext(displayId)) { + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + debugTaskbarManager("dispatchTouchEvent: ", displayId); + // The motion events can be outside the view bounds of task bar, and hence + // manually dispatching them to the drag layer here. + TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId); + if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) { + return taskbar.getDragLayer().dispatchTouchEvent(ev); + } + return super.dispatchTouchEvent(ev); + } + }; + + debugTaskbarManager("createTaskbarRootLayout: adding to map", displayId); + addTaskbarRootLayoutToMap(displayId, newTaskbarRootLayout); + } + + private boolean isDefaultDisplay(int displayId) { + return displayId == mPrimaryDisplayId; + } + + /** + * Retrieves the root layout of the taskbar for the specified display. + * + * @param displayId The ID of the display for which to retrieve the taskbar root layout. + * @return The taskbar root layout {@link FrameLayout} for a given display or {@code null}. + */ + private FrameLayout getTaskbarRootLayoutForDisplay(int displayId) { + debugTaskbarManager("getTaskbarRootLayoutForDisplay:", displayId); + FrameLayout frameLayout = mRootLayouts.get(displayId); + if (frameLayout != null) { + return frameLayout; + } else { + debugTaskbarManager("getTaskbarRootLayoutForDisplay: rootLayout is null!", displayId); + return null; + } + } + + /** + * Adds the taskbar root layout {@link FrameLayout} to taskbar map, mapped to display ID. + * + * @param displayId The ID of the display to associate with the taskbar root layout. + * @param rootLayout The taskbar root layout {@link FrameLayout} to add to the map. + */ + private void addTaskbarRootLayoutToMap(int displayId, FrameLayout rootLayout) { + debugTaskbarManager("addTaskbarRootLayoutToMap: ", displayId); + if (!mRootLayouts.contains(displayId) && rootLayout != null) { + mRootLayouts.put(displayId, rootLayout); + } + + debugTaskbarManager( + "addTaskbarRootLayoutToMap: finished! mRootLayouts.size()=" + mRootLayouts.size(), + displayId); + } + + /** + * Removes taskbar root layout {@link FrameLayout} for given display ID from the taskbar map. + * + * @param displayId The ID of the display for which to remove the taskbar root layout. + */ + private void removeTaskbarRootLayoutFromMap(int displayId) { + debugTaskbarManager("removeTaskbarRootLayoutFromMap:", displayId); + if (mRootLayouts.contains(displayId)) { + mAddedRootLayouts.delete(displayId); + mRootLayouts.delete(displayId); + } + + debugTaskbarManager("removeTaskbarRootLayoutFromMap: finished! mRootLayouts.size=" + + mRootLayouts.size(), displayId); + } + + /** + * Creates {@link Context} for the taskbar on the specified display. + * + * @param displayId The ID of the display for which to create the window context. + */ + private @Nullable Context createWindowContext(int displayId) { + debugTaskbarManager("createWindowContext: ", displayId); + Display display = getDisplay(displayId); + if (display == null) { + debugTaskbarManager("createWindowContext: display null!", displayId); + return null; + } + + int windowType = TYPE_NAVIGATION_BAR_PANEL; + if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && !isExternalDisplay(displayId)) { + windowType = TYPE_NAVIGATION_BAR; + } + debugTaskbarManager( + "createWindowContext: windowType=" + ((windowType == TYPE_NAVIGATION_BAR) + ? "TYPE_NAVIGATION_BAR" : "TYPE_NAVIGATION_BAR_PANEL"), displayId); + + return mBaseContext.createWindowContext(display, windowType, null); + } + + private @Nullable Display getDisplay(int displayId) { + if (mDisplayManager == null) { + debugTaskbarManager("cannot get DisplayManager", displayId); + return null; + } + + Display display = mDisplayManager.getDisplay(displayId); + if (display == null) { + debugTaskbarManager("Cannot get display!", displayId); + return null; + } + + return mDisplayManager.getDisplay(displayId); + } + + /** + * Retrieves the window context of the taskbar for the specified display. + * + * @param displayId The ID of the display for which to retrieve the window context. + * @return The Window Context {@link Context} for a given display or {@code null}. + */ + private Context getWindowContext(int displayId) { + return (!isExternalDisplay(displayId)) + ? mPrimaryWindowContext : mWindowContexts.get(displayId); + } + + @VisibleForTesting + public Context getPrimaryWindowContext() { + return mPrimaryWindowContext; + } + + /** + * Retrieves the window manager {@link WindowManager} of the taskbar for the specified display. + * + * @param displayId The ID of the display for which to retrieve the window manager. + * @return The window manager {@link WindowManager} for a given display or {@code null}. + */ + private @Nullable WindowManager getWindowManager(int displayId) { + if (!isExternalDisplay(displayId)) { + debugTaskbarManager("cannot get mPrimaryWindowManager", displayId); + return mPrimaryWindowManager; + } + + Context externalDisplayContext = getWindowContext(displayId); + if (externalDisplayContext == null) { + debugTaskbarManager("cannot get externalDisplayContext", displayId); + return null; + } + + return externalDisplayContext.getSystemService(WindowManager.class); + } + + /** + * Adds the window context {@link Context} to taskbar map, mapped to display ID. + * + * @param displayId The ID of the display to associate with the taskbar root layout. + * @param windowContext The window context {@link Context} to add to the map. + */ + private void addWindowContextToMap(int displayId, @NonNull Context windowContext) { + if (!mWindowContexts.contains(displayId)) { + mWindowContexts.put(displayId, windowContext); + } + } + + /** + * Removes the window context {@link Context} for given display ID from the taskbar map. + * + * @param displayId The ID of the display for which to remove the taskbar root layout. + */ + private void removeWindowContextFromMap(int displayId) { + if (mWindowContexts.contains(displayId)) { + mWindowContexts.delete(displayId); + } + } + + private boolean isExternalDisplay(int displayId) { + return DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() && ( + mPrimaryDisplayId != displayId); + } + + private int getFocusedDisplayId() { + return SystemUiProxy.INSTANCE.get(mBaseContext).getFocusState().getFocusedDisplayId(); + } + + /** + * Returns the primary display id associated with this manager. + */ + public int getPrimaryDisplayId() { + return mPrimaryDisplayId; + } + + /** + * Logs debug information about the TaskbarManager for primary display. + * + * @param debugReason A string describing the reason for the debug log. + * @param displayId The ID of the display for which to log debug information. + */ + public void debugTaskbarManager(String debugReason, int displayId) { + StringJoiner log = new StringJoiner("\n"); + log.add(debugReason + " displayId=" + displayId + " isDefaultDisplay=" + isDefaultDisplay( + displayId)); + Log.d(TAG, log.toString()); + } + + /** + * Logs verbose debug information about the TaskbarManager for primary display. + * + * @param debugReason A string describing the reason for the debug log. + * @param displayId The ID of the display for which to log debug information. + * @param verbose Indicates whether or not to debug with detail. + */ + private void debugTaskbarManager(String debugReason, int displayId, boolean verbose) { + StringJoiner log = new StringJoiner("\n"); + log.add(debugReason + " displayId=" + displayId + " isDefaultDisplay=" + isDefaultDisplay( + displayId)); + if (verbose) { + generateVerboseLogs(log, displayId); + } + Log.d(TAG, log.toString()); + } + + /** + * Logs debug information about the TaskbarManager for primary display. + * + * @param debugReason A string describing the reason for the debug log. + */ + private void debugPrimaryTaskbar(String debugReason) { + debugTaskbarManager(debugReason, mPrimaryDisplayId, false); + } + + /** + * Logs debug information about the TaskbarManager for primary display. + * + * @param debugReason A string describing the reason for the debug log. + */ + public void debugPrimaryTaskbar(String debugReason, boolean verbose) { + debugTaskbarManager(debugReason, mPrimaryDisplayId, verbose); + } + + /** Creates a {@link PendingIntent} for showing / hiding the all apps UI. */ + public PendingIntent createAllAppsPendingIntent(Executor uiExecutor) { + return new PendingIntent(new AllAppsIntentSender(uiExecutor, this)); + } + + /** + * Logs verbose debug information about the TaskbarManager for a specific display. + */ + private void generateVerboseLogs(StringJoiner log, int displayId) { + boolean activityTaskbarPresent = mActivity != null + && mActivity.getDeviceProfile().isTaskbarPresent; + // TODO (b/381113004): make this display-specific via getWindowContext() + Context windowContext = mPrimaryWindowContext; + if (windowContext == null) { + log.add("windowContext is null!"); + return; + } + + boolean contextTaskbarPresent = false; + if (mUserUnlocked) { + DeviceProfile dp = getDeviceProfile(displayId); + contextTaskbarPresent = dp != null && dp.isTaskbarPresent; + } + if (activityTaskbarPresent == contextTaskbarPresent) { + log.add("mActivity and mWindowContext agree taskbarIsPresent=" + contextTaskbarPresent); + Log.d(TAG, log.toString()); + return; + } + + log.add("mActivity & mWindowContext device profiles have different values, add more logs."); + + log.add("\tmActivity logs:"); + log.add("\t\tmActivity=" + mActivity); + if (mActivity != null) { + log.add("\t\tmActivity.getResources().getConfiguration()=" + + mActivity.getResources().getConfiguration()); + log.add("\t\tmActivity.getDeviceProfile().isTaskbarPresent=" + + activityTaskbarPresent); + } + log.add("\tWindowContext logs:"); + log.add("\t\tWindowContext=" + windowContext); + log.add("\t\tWindowContext.getResources().getConfiguration()=" + + windowContext.getResources().getConfiguration()); + if (mUserUnlocked) { + log.add("\t\tgetDeviceProfile(mPrimaryWindowContext).isTaskbarPresent=" + + contextTaskbarPresent); + } else { + log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked"); + } + } + + private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged = + dp -> debugPrimaryTaskbar("mActivity onDeviceProfileChanged", true); + + /** Use weak reference to avoid leaking TIS via {@link TaskbarManagerImpl} */ + private static class AllAppsIntentSender extends IIntentSender.Stub { + + private final Executor mUiExecutor; + private final WeakReference mWeakTaskbarManager; + + AllAppsIntentSender(Executor uiExecutor, TaskbarManagerImpl taskbarManager) { + mUiExecutor = uiExecutor; + mWeakTaskbarManager = new WeakReference<>(taskbarManager); + } + + @Override + public void send(int i, Intent intent, String s, IBinder iBinder, + IIntentReceiver iIntentReceiver, String s1, Bundle bundle) { + TaskbarManagerImpl taskbarManager = mWeakTaskbarManager.get(); + if (taskbarManager == null) { + return; + } + mUiExecutor.execute(taskbarManager::toggleAllAppsSearch); + } + }; +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManagerImplWrapper.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarManagerImplWrapper.kt new file mode 100644 index 0000000000..f695d8578e --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManagerImplWrapper.kt @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.app.PendingIntent +import android.os.Looper +import com.android.launcher3.Flags.enableTaskbarUiThread +import com.android.launcher3.anim.AnimatorPlaybackController +import com.android.launcher3.statemanager.StatefulActivity +import com.android.launcher3.util.Executors +import com.android.quickstep.views.RecentsViewContainer +import java.io.PrintWriter + +/** + * Wrapper of [TaskbarManagerImpl], this class controls which thread the invocation happens. The + * goal of this class is to minimize the changes to [TaskbarManagerImpl] during migration of + * rendering taskbar in per-window ui thread. + */ +class TaskbarManagerImplWrapper(private val impl: TaskbarManagerImpl) : TaskbarManager { + + override fun onUserUnlocked() { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute(impl::onUserUnlocked) + } else { + impl.onUserUnlocked() + } + } + + override fun setActivity(activity: StatefulActivity<*>) { + // TODO(b/404636836) internal invocation on activity should be posted back to main thread + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.setActivity(activity) } + } else { + impl.setActivity(activity) + } + } + + override fun setRecentsViewContainer(recentsViewContainer: RecentsViewContainer) { + // TODO(b/404636836) internal invocation on recentsViewContainer should be posted back to + // main thread + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.setRecentsViewContainer(recentsViewContainer) } + } else { + impl.setRecentsViewContainer(recentsViewContainer) + } + } + + override fun recreateTaskbars() { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute(impl::recreateTaskbars) + } else { + impl.recreateTaskbars() + } + } + + override fun onSystemUiFlagsChanged(systemUiStateFlags: Long, displayId: Int) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { + impl.onSystemUiFlagsChanged(systemUiStateFlags, displayId) + } + } else { + impl.onSystemUiFlagsChanged(systemUiStateFlags, displayId) + } + } + + override fun onLongPressHomeEnabled(assistantLongPressEnabled: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { + impl.onLongPressHomeEnabled(assistantLongPressEnabled) + } + } else { + impl.onLongPressHomeEnabled(assistantLongPressEnabled) + } + } + + override fun setSetupUIVisible(isVisible: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.setSetupUIVisible(isVisible) } + } else { + impl.setSetupUIVisible(isVisible) + } + } + + override fun setWallpaperVisible(displayId: Int, isVisible: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.setWallpaperVisible(displayId, isVisible) } + } else { + impl.setWallpaperVisible(displayId, isVisible) + } + } + + override fun checkNavBarModes(displayId: Int) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.checkNavBarModes(displayId) } + } else { + impl.checkNavBarModes(displayId) + } + } + + override fun finishBarAnimations(displayId: Int) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.finishBarAnimations(displayId) } + } else { + impl.finishBarAnimations(displayId) + } + } + + override fun touchAutoDim(displayId: Int, reset: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.touchAutoDim(displayId, reset) } + } else { + impl.touchAutoDim(displayId, reset) + } + } + + override fun transitionTo(displayId: Int, barMode: Int, animate: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.transitionTo(displayId, barMode, animate) } + } else { + impl.transitionTo(displayId, barMode, animate) + } + } + + override fun appTransitionPending(pending: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.appTransitionPending(pending) } + } else { + impl.appTransitionPending(pending) + } + } + + override fun onRotationProposal(rotation: Int, isValid: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.onRotationProposal(rotation, isValid) } + } else { + impl.onRotationProposal(rotation, isValid) + } + } + + override fun disableNavBarElements(displayId: Int, state1: Int, state2: Int, animate: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { + impl.disableNavBarElements(displayId, state1, state2, animate) + } + } else { + impl.disableNavBarElements(displayId, state1, state2, animate) + } + } + + override fun onSystemBarAttributesChanged(displayId: Int, behavior: Int) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { + impl.onSystemBarAttributesChanged(displayId, behavior) + } + } else { + impl.onSystemBarAttributesChanged(displayId, behavior) + } + } + + override fun onTransitionModeUpdated(barMode: Int, checkBarModes: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { + impl.onTransitionModeUpdated(barMode, checkBarModes) + } + } else { + impl.onTransitionModeUpdated(barMode, checkBarModes) + } + } + + override fun onNavButtonsDarkIntensityChanged(darkIntensity: Float) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { + impl.onNavButtonsDarkIntensityChanged(darkIntensity) + } + } else { + impl.onNavButtonsDarkIntensityChanged(darkIntensity) + } + } + + override fun onNavigationBarLumaSamplingEnabled(displayId: Int, enable: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { + impl.onNavigationBarLumaSamplingEnabled(displayId, enable) + } + } else { + impl.onNavigationBarLumaSamplingEnabled(displayId, enable) + } + } + + override fun onDisplayAddSystemDecorations(displayId: Int) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.onDisplayAddSystemDecorations(displayId) } + } else { + impl.onDisplayAddSystemDecorations(displayId) + } + } + + override fun onDisplayRemoved(displayId: Int) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.onDisplayRemoved(displayId) } + } else { + impl.onDisplayRemoved(displayId) + } + } + + override fun onDisplayRemoveSystemDecorations(displayId: Int) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.onDisplayRemoveSystemDecorations(displayId) } + } else { + impl.onDisplayRemoveSystemDecorations(displayId) + } + } + + override fun destroy() { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.destroy() } + } else { + impl.destroy() + } + } + + override fun createLauncherStartFromSuwAnim(duration: Int): AnimatorPlaybackController? { + // TODO(b/404636836): Evaluate if internal impl taskbar.createLauncherStartFromSuwAnim() is + // thread safe + return impl.createLauncherStartFromSuwAnim(duration) + } + + override fun getCurrentActivityContext(): TaskbarActivityContext? { + // Thread safe + return impl.currentActivityContext + } + + override fun getUIControllerForDisplay(displayId: Int): TaskbarUIController? { + // Thread safe + return impl.getUIControllerForDisplay(displayId) + } + + override fun getTaskbarForDisplay(displayId: Int): TaskbarActivityContext? { + // Thread safe + return impl.getTaskbarForDisplay(displayId) + } + + override fun createAllAppsPendingIntent(): PendingIntent { + // Thread safe + return impl.createAllAppsPendingIntent( + if (enableTaskbarUiThread()) impl.perWindowUiExecutor else Executors.MAIN_EXECUTOR + ) + } + + override fun getPrimaryDisplayId(): Int { + // Thread safe + return impl.getPrimaryDisplayId() + } + + override fun dumpLogs(prefix: String, pw: PrintWriter) { + // Stay on caller thread because PrinterWriter is not thread safe. + impl.dumpLogs(prefix, pw) + } + + override fun debugPrimaryTaskbar(debugReason: String, verbose: Boolean) { + if (shouldPostToTaskbarUiThread()) { + impl.perWindowUiExecutor.execute { impl.debugPrimaryTaskbar(debugReason, verbose) } + } else { + impl.debugPrimaryTaskbar(debugReason, verbose) + } + } + + private fun shouldPostToTaskbarUiThread(): Boolean { + return enableTaskbarUiThread() && Looper.getMainLooper() == Looper.myLooper() + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index b065124c97..15807ae547 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -15,36 +15,35 @@ */ package com.android.launcher3.taskbar; -import static com.android.window.flags2.Flags.enableDesktopWindowingMode; -import static com.android.window.flags2.Flags.enableDesktopWindowingTaskbarRunningApps; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import android.util.SparseArray; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.celllayout.CellInfo; import com.android.launcher3.model.BgDataModel; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.statehandlers.DesktopVisibilityController; +import com.android.launcher3.model.data.PredictedContainerInfo; +import com.android.launcher3.model.data.WorkspaceData; +import com.android.launcher3.taskbar.TaskbarView.TaskbarLayoutParams; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.IntArray; -import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; -import com.android.quickstep.LauncherActivityInterface; -import com.android.quickstep.RecentsModel; +import com.android.quickstep.util.GroupTask; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -54,7 +53,7 @@ import java.util.function.Predicate; * Launcher model Callbacks for rendering taskbar. */ public class TaskbarModelCallbacks implements - BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener { + BgDataModel.Callbacks, LauncherBindableItemsContainer { private final SparseArray mHotseatItems = new SparseArray<>(); private List mPredictedItems = Collections.emptyList(); @@ -68,8 +67,6 @@ public class TaskbarModelCallbacks implements // Used to defer any UI updates during the SUW unstash animation. private boolean mDeferUpdatesForSUW; private Runnable mDeferredUpdates; - private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener = - visible -> updateRunningApps(); public TaskbarModelCallbacks( TaskbarActivityContext context, TaskbarView container) { @@ -79,72 +76,28 @@ public class TaskbarModelCallbacks implements public void init(TaskbarControllers controllers) { mControllers = controllers; - if (mControllers.taskbarRecentAppsController.getCanShowRunningApps()) { - RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this); - - if (shouldShowRunningAppsInDesktopMode()) { - DesktopVisibilityController desktopVisibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (desktopVisibilityController != null) { - desktopVisibilityController.registerDesktopVisibilityListener( - mDesktopVisibilityListener); - } - } - } - } - - /** - * Unregisters listeners in this class. - */ - public void unregisterListeners() { - RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener(); - - if (shouldShowRunningAppsInDesktopMode()) { - DesktopVisibilityController desktopVisibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (desktopVisibilityController != null) { - desktopVisibilityController.unregisterDesktopVisibilityListener( - mDesktopVisibilityListener); - } - } - } - - private boolean shouldShowRunningAppsInDesktopMode() { - // TODO(b/335401172): unify DesktopMode checks in Launcher - return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps(); } @Override - public void startBinding() { - mContext.setBindingItems(true); + public void bindCompleteModel(WorkspaceData itemIdMap, boolean isBindingSync) { mHotseatItems.clear(); - mPredictedItems = Collections.emptyList(); - } + mPredictedItems = itemIdMap.getPredictedContents(CONTAINER_HOTSEAT_PREDICTION); + handleItemsAdded(itemIdMap); - @Override - public void finishBindingItems(IntSet pagesBoundFirst) { - mContext.setBindingItems(false); + if (itemIdMap.get(CONTAINER_ALL_APPS_PREDICTION) instanceof PredictedContainerInfo pci) { + mControllers.taskbarAllAppsController.setPredictedApps(pci.getContents()); + } commitItemsToUI(); } @Override - public void bindAppsAdded(IntArray newScreens, ArrayList addNotAnimated, - ArrayList addAnimated) { - boolean add1 = handleItemsAdded(addNotAnimated); - boolean add2 = handleItemsAdded(addAnimated); - if (add1 || add2) { + public void bindItemsAdded(List items) { + if (handleItemsAdded(items)) { commitItemsToUI(); } } - @Override - public void bindItems(List shortcuts, boolean forceAnimateIcons) { - if (handleItemsAdded(shortcuts)) { - commitItemsToUI(); - } - } - - private boolean handleItemsAdded(List items) { + private boolean handleItemsAdded(Iterable items) { boolean modified = false; for (ItemInfo item : items) { if (item.container == Favorites.CONTAINER_HOTSEAT) { @@ -155,26 +108,49 @@ public class TaskbarModelCallbacks implements return modified; } - @Override - public void bindWorkspaceItemsChanged(List updated) { - updateWorkspaceItems(updated, mContext); + public void bindItemsUpdated(@NonNull Set updates) { + Set itemsToRebind = updateContainerItems(updates, mContext); + boolean removed = handleItemsRemoved(ItemInfoMatcher.ofItems(itemsToRebind)); + boolean added = handleItemsAdded(itemsToRebind); + + boolean predictionsUpdated = false; + for (ItemInfo update: updates) { + if (update instanceof PredictedContainerInfo pci) { + if (pci.id == Favorites.CONTAINER_HOTSEAT_PREDICTION) { + mPredictedItems = pci.getContents(); + predictionsUpdated = true; + } else if (pci.id == CONTAINER_ALL_APPS_PREDICTION) { + mControllers.taskbarAllAppsController.setPredictedApps(pci.getContents()); + } + } + } + if (removed || added || predictionsUpdated) { + commitItemsToUI(); + } + } + + @Nullable + @Override + public CellInfo getCellInfoForView(@NonNull View view) { + return view.getLayoutParams() instanceof TaskbarLayoutParams tlp ? tlp.bindInfo : null; } @Override - public void bindRestoreItemsChange(HashSet updates) { - updateRestoreItems(updates, mContext); + public boolean isContainerSupported(int container) { + return container == CONTAINER_HOTSEAT || container == CONTAINER_HOTSEAT_PREDICTION; } @Override - public void mapOverItems(ItemOperator op) { + public View mapOverItems(@NonNull ItemOperator op) { final int itemCount = mContainer.getChildCount(); for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) { View item = mContainer.getChildAt(itemIdx); - if (op.evaluate((ItemInfo) item.getTag(), item)) { - return; + if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) { + return item; } } + return null; } @Override @@ -195,30 +171,7 @@ public class TaskbarModelCallbacks implements return modified; } - @Override - public void bindItemsModified(List items) { - boolean removed = handleItemsRemoved(ItemInfoMatcher.ofItems(items)); - boolean added = handleItemsAdded(items); - if (removed || added) { - commitItemsToUI(); - } - } - - @Override - public void bindExtraContainerItems(FixedContainerItems item) { - if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { - mPredictedItems = item.items; - commitItemsToUI(); - } else if (item.containerId == Favorites.CONTAINER_PREDICTION) { - mControllers.taskbarAllAppsController.setPredictedApps(item.items); - } - } - private void commitItemsToUI() { - if (mContext.isBindingItems()) { - return; - } - ItemInfo[] hotseatItemInfos = new ItemInfo[mContext.getDeviceProfile().numShownHotseatIcons]; int predictionSize = mPredictedItems.size(); @@ -232,26 +185,26 @@ public class TaskbarModelCallbacks implements predictionNextIndex++; } } - hotseatItemInfos = mControllers.taskbarRecentAppsController - .updateHotseatItemInfos(hotseatItemInfos); - Set runningPackages = mControllers.taskbarRecentAppsController.getRunningApps(); - Set minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps(); + + final TaskbarRecentAppsController recentAppsController = + mControllers.taskbarRecentAppsController; + hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos); if (mDeferUpdatesForSUW) { ItemInfo[] finalHotseatItemInfos = hotseatItemInfos; mDeferredUpdates = () -> - commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages, - minimizedPackages); + commitHotseatItemUpdates(finalHotseatItemInfos, + recentAppsController.getShownTasks()); } else { - commitHotseatItemUpdates(hotseatItemInfos, runningPackages, minimizedPackages); + commitHotseatItemUpdates(hotseatItemInfos, recentAppsController.getShownTasks()); } } - private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, Set runningPackages, - Set minimizedPackages) { - mContainer.updateHotseatItems(hotseatItemInfos); - mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages, - minimizedPackages); + private void commitHotseatItemUpdates( + ItemInfo[] hotseatItemInfos, List recentTasks) { + mContainer.updateItems(hotseatItemInfos, recentTasks); + mControllers.taskbarViewController.updateIconViewsRunningStates(); + mControllers.taskbarPopupController.setHotseatInfosList(mHotseatItems); } /** @@ -270,21 +223,11 @@ public class TaskbarModelCallbacks implements } } - @Override - public void onRunningTasksChanged() { - updateRunningApps(); - } - /** Called when there's a change in running apps to update the UI. */ public void commitRunningAppsToUI() { commitItemsToUI(); } - /** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */ - public void updateRunningApps() { - mControllers.taskbarRecentAppsController.updateRunningApps(); - } - @Override public void bindDeepShortcutMap(HashMap deepShortcutMapCopy) { mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy); @@ -296,7 +239,7 @@ public class TaskbarModelCallbacks implements Map packageUserKeytoUidMap) { Preconditions.assertUIThread(); mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap); - mControllers.taskbarRecentAppsController.setApps(apps); + mControllers.taskbarPopupController.setApps(apps); } protected void dumpLogs(String prefix, PrintWriter pw) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacksFactory.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacksFactory.kt index eb03b4abc5..fbf8266e69 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacksFactory.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacksFactory.kt @@ -17,12 +17,11 @@ package com.android.launcher3.taskbar import android.content.Context -import com.android.launcher3.R -import com.android.launcher3.util.ResourceBasedOverride -import com.android.launcher3.util.ResourceBasedOverride.Overrides +import com.android.launcher3.dagger.LauncherComponentProvider +import javax.inject.Inject /** Creates [TaskbarModelCallbacks] instances. */ -open class TaskbarModelCallbacksFactory : ResourceBasedOverride { +open class TaskbarModelCallbacksFactory @Inject constructor() { open fun create( activityContext: TaskbarActivityContext, @@ -32,11 +31,7 @@ open class TaskbarModelCallbacksFactory : ResourceBasedOverride { companion object { @JvmStatic fun newInstance(context: Context): TaskbarModelCallbacksFactory { - return Overrides.getObject( - TaskbarModelCallbacksFactory::class.java, - context, - R.string.taskbar_model_callbacks_factory_class, - ) + return LauncherComponentProvider.get(context).getTaskbarModelCallbacksFactory() } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index d26a36d175..7fd8b4340b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -16,6 +16,9 @@ package com.android.launcher3.taskbar; +import static android.view.KeyEvent.ACTION_DOWN; +import static android.view.KeyEvent.ACTION_UP; + import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS; import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS; @@ -24,19 +27,25 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; +import static com.android.window.flags.Flags.predictiveBackThreeButtonNav; import android.content.Context; +import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; +import android.os.SystemClock; import android.util.Log; import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.Flags; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -48,7 +57,8 @@ import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import java.io.PrintWriter; @@ -70,6 +80,7 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa private long mLastScreenPinLongPress; private boolean mScreenPinned; private boolean mAssistantLongPressEnabled; + private int mLastSentBackAction = ACTION_UP; @Override public void dumpLogs(String prefix, PrintWriter pw) { @@ -77,6 +88,8 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress); pw.println(prefix + "\tmScreenPinned=" + mScreenPinned); + pw.println(prefix + "\tmLastSentBackAction=" + + KeyEvent.actionToString(mLastSentBackAction)); } @Retention(RetentionPolicy.SOURCE) @@ -108,7 +121,8 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa private final TaskbarNavButtonCallbacks mCallbacks; private final SystemUiProxy mSystemUiProxy; private final Handler mHandler; - private final AssistUtils mAssistUtils; + private final ContextualSearchInvoker mContextualSearchInvoker; + private TaskbarControllers mControllers; @Nullable private StatsLogManager mStatsLogManager; private final Runnable mResetLongPress = this::resetScreenUnpin; @@ -118,36 +132,50 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa TaskbarNavButtonCallbacks callbacks, SystemUiProxy systemUiProxy, Handler handler, - AssistUtils assistUtils) { + ContextualSearchInvoker contextualSearchInvoker) { mContext = context; mCallbacks = callbacks; mSystemUiProxy = systemUiProxy; mHandler = handler; - mAssistUtils = assistUtils; + mContextualSearchInvoker = contextualSearchInvoker; } public void onButtonClick(@TaskbarButton int buttonType, View view) { if (buttonType == BUTTON_SPACE) { return; } + boolean predictiveBackThreeButtonNav; + try { + predictiveBackThreeButtonNav = predictiveBackThreeButtonNav(); + } catch (Throwable t) { + predictiveBackThreeButtonNav = false; + } + if (predictiveBackThreeButtonNav && mLastSentBackAction == ACTION_DOWN) { + Log.i(TAG, "Button click ignored while back button is pressed"); + // prevent interactions with other buttons while back button is pressed + return; + } // Provide the same haptic feedback that the system offers for virtual keys. view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); switch (buttonType) { case BUTTON_BACK: - logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); - executeBack(); + executeBack(/* keyEvent */ null); break; case BUTTON_HOME: logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP); + mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false, + GestureType.HOME); navigateHome(); break; case BUTTON_RECENTS: logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP); + mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false, + GestureType.OVERVIEW); navigateToOverview(); break; case BUTTON_IME_SWITCH: logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); - showIMESwitcher(); + onImeSwitcherPress(); break; case BUTTON_A11Y: logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP); @@ -162,28 +190,63 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa } } + /** + * Handles long clicks and plays haptics for user visible actions. + */ public boolean onButtonLongClick(@TaskbarButton int buttonType, View view) { if (buttonType == BUTTON_SPACE) { return false; } - // Provide the same haptic feedback that the system offers for virtual keys. - view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + boolean predictiveBackThreeButtonNav; + try { + predictiveBackThreeButtonNav = predictiveBackThreeButtonNav(); + } catch (Throwable t) { + predictiveBackThreeButtonNav = false; + } + if (predictiveBackThreeButtonNav && mLastSentBackAction == ACTION_DOWN + && buttonType != BUTTON_BACK && buttonType != BUTTON_RECENTS) { + // prevent interactions with other buttons while back button is pressed (except back + // and recents button for screen-unpin action). + Log.i(TAG, "Button long click ignored while back button is pressed"); + return false; + } + switch (buttonType) { case BUTTON_HOME: logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); onLongPressHome(); + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); return true; case BUTTON_A11Y: logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS); notifyA11yClick(true /* longClick */); + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); return true; case BUTTON_BACK: logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS); - return backRecentsLongpress(buttonType); + if (backRecentsLongpress(buttonType)) { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + return true; case BUTTON_RECENTS: logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS); - return backRecentsLongpress(buttonType); + if (backRecentsLongpress(buttonType)) { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + return true; case BUTTON_IME_SWITCH: + if (Flags.imeSwitcherRevamp()) { + logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); + onImeSwitcherLongPress(); + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + return true; + } + return false; default: return false; } @@ -210,6 +273,13 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa } } + /** + * Notifies SystemUI of the new bounds of the recents button in screen coordinates. + */ + public void onRecentsButtonLayoutChanged(Rect bounds) { + mSystemUiProxy.notifyRecentsButtonPositionChanged(bounds); + } + private boolean backRecentsLongpress(@TaskbarButton int buttonType) { mLongPressedButtons |= buttonType; return determineScreenUnpin(); @@ -253,6 +323,10 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa } private void resetScreenUnpin() { + // if only back button was long pressed, navigate back like a single click back behavior. + if (mLongPressedButtons == BUTTON_BACK) { + executeBack(null); + } mLongPressedButtons = 0; mLastScreenPinLongPress = 0; } @@ -262,7 +336,8 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa } public void init(TaskbarControllers taskbarControllers) { - mStatsLogManager = taskbarControllers.getTaskbarActivityContext().getStatsLogManager(); + mControllers = taskbarControllers; + mStatsLogManager = mControllers.getTaskbarActivityContext().getStatsLogManager(); } public void onDestroy() { @@ -283,7 +358,7 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa private void navigateHome() { TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY); - mCallbacks.onNavigateHome(); + mCallbacks.onNavigateHome(mContext.getDisplayId()); } private void navigateToOverview() { @@ -292,17 +367,45 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa } TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle"); TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); - mCallbacks.onToggleOverview(); + mCallbacks.onToggleOverview(mContext.getDisplayId()); } - private void executeBack() { - mSystemUiProxy.onBackPressed(); + public void hideOverview() { + mCallbacks.onHideOverview(mContext.getDisplayId()); } - private void showIMESwitcher() { + void sendBackKeyEvent(int action, boolean cancelled) { + if (action == mLastSentBackAction) { + // There must always be an alternating sequence of ACTION_DOWN and ACTION_UP events + return; + } + long time = SystemClock.uptimeMillis(); + KeyEvent keyEvent = new KeyEvent(time, time, action, KeyEvent.KEYCODE_BACK, 0); + keyEvent.setDisplayId(mControllers.getTaskbarActivityContext().getDisplayId()); + if (cancelled) { + keyEvent.cancel(); + } + executeBack(keyEvent); + } + + private void executeBack(@Nullable KeyEvent keyEvent) { + if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) { + logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); + mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false, + GestureType.BACK); + } + mSystemUiProxy.onBackEvent(keyEvent); + mLastSentBackAction = keyEvent != null ? keyEvent.getAction() : ACTION_UP; + } + + private void onImeSwitcherPress() { mSystemUiProxy.onImeSwitcherPressed(); } + private void onImeSwitcherLongPress() { + mSystemUiProxy.onImeSwitcherLongPress(); + } + private void notifyA11yClick(boolean longClick) { if (longClick) { mSystemUiProxy.notifyAccessibilityButtonLongClicked(); @@ -315,8 +418,9 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa if (mScreenPinned || !mAssistantLongPressEnabled) { return; } - // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation. - if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { + // Attempt to start Contextual Search, otherwise fall back to SysUi's implementation. + if (!mContextualSearchInvoker.tryStartAssistOverride( + INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { Bundle args = new Bundle(); args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); mSystemUiProxy.startAssistant(args); @@ -334,9 +438,12 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa /** Callbacks for navigation buttons on Taskbar. */ public interface TaskbarNavButtonCallbacks { /** Callback invoked when the home button is pressed. */ - default void onNavigateHome() {} + default void onNavigateHome(int displayId) {} /** Callback invoked when the overview button is pressed. */ - default void onToggleOverview() {} + default void onToggleOverview(int displayId) {} + + /** Callback invoken when a visible overview needs to be hidden. */ + default void onHideOverview(int displayId) { } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java new file mode 100644 index 0000000000..34787641c2 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java @@ -0,0 +1,508 @@ +/* + * 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.launcher3.taskbar; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.BlendMode; +import android.graphics.BlendModeColorFilter; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.IntProperty; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.ColorUtils; + +import com.android.app.animation.Interpolators; +import com.android.launcher3.R; +import com.android.launcher3.Reorderable; +import com.android.launcher3.Utilities; +import com.android.launcher3.icons.IconNormalizer; +import com.android.launcher3.util.MultiTranslateDelegate; +import com.android.launcher3.util.Themes; +import com.android.systemui.shared.recents.model.Task; + +import java.util.ArrayList; +import java.util.List; + +/** + * View used as overflow icon within task bar, when the list of recent/running apps overflows the + * available display bounds - if display is not wide enough to show all running apps in the taskbar, + * this icon is added to the taskbar as an entry point to open UI that surfaces all running apps. + * The icon contains icon representations of up to 4 more recent tasks in overflow, stacked on top + * each other in counter clockwise manner (icons of tasks partially overlapping with each other). + */ +public class TaskbarOverflowView extends FrameLayout implements Reorderable { + private static final int ALPHA_TRANSPARENT = 0; + private static final int ALPHA_OPAQUE = 255; + private static final long ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND = 300L; + private static final long ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS = 500L; + private static final long ANIMATION_SET_DURATION = 1000L; + private static final long ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION = 500L; + private static final long ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION = 600L; + private static final long ITEM_ICON_SIZE_ANIMATION_DURATION = 500L; + private static final long ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION = 500L; + private static final long LEAVE_BEHIND_ANIMATIONS_DELAY = 500L; + private static final long LEAVE_BEHIND_OPACITY_ANIMATION_DURATION = 100L; + private static final long LEAVE_BEHIND_SIZE_ANIMATION_DURATION = 500L; + private static final float LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER = 0.83f; + private static final int MAX_ITEMS_IN_PREVIEW = 4; + + // The height divided by the width of the horizontal box containing two overlapping app icons. + // According to the spec, this ratio is constant for different sizes of taskbar app icons. + // Assuming the width of this box = taskbar app icon size - 2 paddings - 2 stroke widths, and + // the height = width * 0.61, which is also equal to the height of a single item in the preview. + private static final float TWO_ITEM_ICONS_BOX_ASPECT_RATIO = 0.61f; + + private static final FloatProperty ITEM_ICON_CENTER_OFFSET = + new FloatProperty<>("itemIconCenterOffset") { + @Override + public Float get(TaskbarOverflowView view) { + return view.mItemIconCenterOffset; + } + + @Override + public void setValue(TaskbarOverflowView view, float value) { + view.mItemIconCenterOffset = value; + view.invalidate(); + } + }; + + private static final IntProperty ITEM_ICON_COLOR_FILTER_OPACITY = + new IntProperty<>("itemIconColorFilterOpacity") { + @Override + public Integer get(TaskbarOverflowView view) { + return view.mItemIconColorFilterOpacity; + } + + @Override + public void setValue(TaskbarOverflowView view, int value) { + view.mItemIconColorFilterOpacity = value; + view.invalidate(); + } + }; + + private static final FloatProperty ITEM_ICON_SIZE = + new FloatProperty<>("itemIconSize") { + @Override + public Float get(TaskbarOverflowView view) { + return view.mItemIconSize; + } + + @Override + public void setValue(TaskbarOverflowView view, float value) { + view.mItemIconSize = value; + view.invalidate(); + } + }; + + private static final FloatProperty ITEM_ICON_STROKE_WIDTH = + new FloatProperty<>("itemIconStrokeWidth") { + @Override + public Float get(TaskbarOverflowView view) { + return view.mItemIconStrokeWidth; + } + + @Override + public void setValue(TaskbarOverflowView view, float value) { + view.mItemIconStrokeWidth = value; + view.invalidate(); + } + }; + + private static final IntProperty LEAVE_BEHIND_OPACITY = + new IntProperty<>("leaveBehindOpacity") { + @Override + public Integer get(TaskbarOverflowView view) { + return view.mLeaveBehindOpacity; + } + + @Override + public void setValue(TaskbarOverflowView view, int value) { + view.mLeaveBehindOpacity = value; + view.invalidate(); + } + }; + + private static final FloatProperty LEAVE_BEHIND_SIZE = + new FloatProperty<>("leaveBehindSize") { + @Override + public Float get(TaskbarOverflowView view) { + return view.mLeaveBehindSize; + } + + @Override + public void setValue(TaskbarOverflowView view, float value) { + view.mLeaveBehindSize = value; + view.invalidate(); + } + }; + + private boolean mIsRtlLayout; + private final List mItems = new ArrayList(); + private int mIconSize; + private Paint mItemBackgroundPaint; + private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); + private float mScaleForReorderBounce = 1f; + private int mItemBackgroundColor; + private int mLeaveBehindColor; + + // Active means the overflow icon has been pressed, which replaces the app icons with the + // leave-behind circle and shows the KQS UI. + private boolean mIsActive = false; + private ValueAnimator mStateTransitionAnimationWrapper; + + private float mItemIconCenterOffsetDefault; + private float mItemIconCenterOffset; // [0..mItemIconCenterOffsetDefault] + private int mItemIconColorFilterOpacity; // [ALPHA_TRANSPARENT..ALPHA_OPAQUE] + private float mItemIconSizeDefault; + private float mItemIconSizeScaledDown; + private float mItemIconSize; // [mItemIconSizeScaledDown..mItemIconSizeDefault] + private float mItemIconStrokeWidthDefault; + private float mItemIconStrokeWidth; // [0..mItemIconStrokeWidthDefault] + private int mLeaveBehindOpacity; // [ALPHA_TRANSPARENT..ALPHA_OPAQUE] + private float mLeaveBehindSizeScaledDown; + private float mLeaveBehindSizeDefault; + private float mLeaveBehindSize; // [mLeaveBehindSizeScaledDown..mLeaveBehindSizeDefault] + + public TaskbarOverflowView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public TaskbarOverflowView(Context context) { + super(context); + init(); + } + + /** + * Inflates the taskbar overflow button view. + * @param resId The resource to inflate the view from. + * @param group The parent view. + * @param iconSize The size of the overflow button icon. + * @param padding The internal padding of the overflow view. + * @return A taskbar overflow button. + */ + public static TaskbarOverflowView inflateIcon(int resId, ViewGroup group, int iconSize, + int padding) { + LayoutInflater inflater = LayoutInflater.from(group.getContext()); + TaskbarOverflowView icon = (TaskbarOverflowView) inflater.inflate(resId, group, false); + + icon.mIconSize = iconSize; + + final float taskbarIconRadius = + (iconSize - padding * 2f) * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f; + + icon.mLeaveBehindSizeDefault = taskbarIconRadius; // 1/2 of taskbar app icon size + icon.mLeaveBehindSizeScaledDown = + icon.mLeaveBehindSizeDefault * LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER; + icon.mLeaveBehindSize = icon.mLeaveBehindSizeScaledDown; + + icon.mItemIconStrokeWidthDefault = + taskbarIconRadius / 10f; // 1/20 of taskbar app icon size + icon.mItemIconStrokeWidth = icon.mItemIconStrokeWidthDefault; + + icon.mItemIconSizeDefault = 2f * taskbarIconRadius * TWO_ITEM_ICONS_BOX_ASPECT_RATIO; + icon.mItemIconSizeScaledDown = icon.mLeaveBehindSizeScaledDown; + icon.mItemIconSize = icon.mItemIconSizeDefault; + + icon.mItemIconCenterOffsetDefault = taskbarIconRadius + - icon.mItemIconSizeDefault * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f + - icon.mItemIconStrokeWidthDefault; + icon.mItemIconCenterOffset = icon.mItemIconCenterOffsetDefault; + + return icon; + } + + private void init() { + mIsRtlLayout = Utilities.isRtl(getResources()); + mItemBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mItemBackgroundColor = getContext().getColor( + com.android.internal.R.color.materialColorInverseOnSurface); + mLeaveBehindColor = Themes.getAttrColor(getContext(), android.R.attr.textColorTertiary); + + setWillNotDraw(false); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + + drawAppIcons(canvas); + drawLeaveBehindCircle(canvas); + } + + private void drawAppIcons(@NonNull Canvas canvas) { + mItemBackgroundPaint.setColor(mItemBackgroundColor); + float canvasCenterXY = mIconSize / 2f; + int adjustedItemIconSize = Math.round(mItemIconSize); + float itemIconRadius = adjustedItemIconSize / 2f; + + int itemsToShow = Math.min(mItems.size(), MAX_ITEMS_IN_PREVIEW); + for (int i = itemsToShow - 1; i >= 0; --i) { + Drawable icon = mItems.get(mItems.size() - i - 1).icon; + if (icon == null) { + continue; + } + + float itemCenterX = getItemXOffset(mItemIconCenterOffset, mIsRtlLayout, i, itemsToShow); + float itemCenterY = getItemYOffset(mItemIconCenterOffset, i, itemsToShow); + + Drawable iconCopy = icon.getConstantState().newDrawable().mutate(); + iconCopy.setBounds(0, 0, adjustedItemIconSize, adjustedItemIconSize); + iconCopy.setColorFilter(new BlendModeColorFilter( + ColorUtils.setAlphaComponent(mLeaveBehindColor, mItemIconColorFilterOpacity), + BlendMode.SRC_ATOP)); + + canvas.save(); + canvas.translate( + canvasCenterXY + itemCenterX - itemIconRadius, + canvasCenterXY + itemCenterY - itemIconRadius); + canvas.drawCircle(itemIconRadius, itemIconRadius, + itemIconRadius * IconNormalizer.ICON_VISIBLE_AREA_FACTOR + mItemIconStrokeWidth, + mItemBackgroundPaint); + iconCopy.draw(canvas); + canvas.restore(); + } + } + + private void drawLeaveBehindCircle(@NonNull Canvas canvas) { + mItemBackgroundPaint.setColor( + ColorUtils.setAlphaComponent(mLeaveBehindColor, mLeaveBehindOpacity)); + + final float xyCenter = mIconSize / 2f; + canvas.drawCircle(xyCenter, xyCenter, mLeaveBehindSize / 2f, mItemBackgroundPaint); + } + + /** + * Clears the list of tasks tracked by the view. + */ + public void clearItems() { + mItems.clear(); + invalidate(); + } + + /** + * Update the view to represent a new list of recent tasks. + * @param items Items to be shown in the view. + */ + public void setItems(List items) { + mItems.clear(); + mItems.addAll(items); + invalidate(); + } + + @VisibleForTesting + public List getItemIds() { + return mItems.stream().map(task -> task.key.id).toList(); + } + + /** + * Called when a task is updated. If the task is contained within the view, it's cached value + * gets updated. If the task is shown within the icon, invalidates the view, so the task icon + * gets updated. + * @param task The updated task. + */ + public void updateTaskIsShown(Task task) { + for (int i = 0; i < mItems.size(); ++i) { + if (mItems.get(i).key.id == task.key.id) { + mItems.set(i, task); + if (i >= mItems.size() - MAX_ITEMS_IN_PREVIEW) { + invalidate(); + } + break; + } + } + } + + /** + * @return Tooltip to be used for the taskbar overflow view - returns null if the view should + * not have a tooltip. + */ + public String getTextForTooltipPopup() { + if (mIsActive) { + return null; + } + return getResources().getString(R.string.taskbar_overflow_a11y_title); + } + + /** + * Returns the view's state (whether it shows a set of app icons or a leave-behind circle). + */ + public boolean getIsActive() { + return mIsActive; + } + + /** + * Updates the view's state to draw either a set of app icons or a leave-behind circle. + * @param isActive The next state of the view. + */ + public void setIsActive(boolean isActive) { + if (mIsActive == isActive) { + return; + } + mIsActive = isActive; + + if (mStateTransitionAnimationWrapper != null + && mStateTransitionAnimationWrapper.isRunning()) { + mStateTransitionAnimationWrapper.reverse(); + return; + } + + final AnimatorSet stateTransitionAnimation = getStateTransitionAnimation(); + mStateTransitionAnimationWrapper = ValueAnimator.ofFloat(0, 1f); + mStateTransitionAnimationWrapper.setDuration(mIsActive + ? ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND + : ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS); + mStateTransitionAnimationWrapper.setInterpolator( + mIsActive ? Interpolators.STANDARD : Interpolators.EMPHASIZED); + mStateTransitionAnimationWrapper.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mStateTransitionAnimationWrapper = null; + } + }); + mStateTransitionAnimationWrapper.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + stateTransitionAnimation.setCurrentPlayTime( + (long) (ANIMATION_SET_DURATION * animator.getAnimatedFraction())); + } + }); + mStateTransitionAnimationWrapper.start(); + } + + private AnimatorSet getStateTransitionAnimation() { + final AnimatorSet animation = new AnimatorSet(); + animation.setInterpolator(Interpolators.LINEAR); + animation.playTogether( + buildAnimator(ITEM_ICON_CENTER_OFFSET, 0f, mItemIconCenterOffsetDefault, + ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION, 0L, + ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION), + buildAnimator(ITEM_ICON_COLOR_FILTER_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT, + ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION, 0L, + ANIMATION_SET_DURATION - ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION), + buildAnimator(ITEM_ICON_SIZE, mItemIconSizeScaledDown, mItemIconSizeDefault, + ITEM_ICON_SIZE_ANIMATION_DURATION, 0L, + ITEM_ICON_SIZE_ANIMATION_DURATION), + buildAnimator(ITEM_ICON_STROKE_WIDTH, 0f, mItemIconStrokeWidthDefault, + ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION, 0L, + ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION), + buildAnimator(LEAVE_BEHIND_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT, + LEAVE_BEHIND_OPACITY_ANIMATION_DURATION, LEAVE_BEHIND_ANIMATIONS_DELAY, + ANIMATION_SET_DURATION - LEAVE_BEHIND_ANIMATIONS_DELAY + - LEAVE_BEHIND_OPACITY_ANIMATION_DURATION), + buildAnimator(LEAVE_BEHIND_SIZE, mLeaveBehindSizeDefault, + mLeaveBehindSizeScaledDown, LEAVE_BEHIND_SIZE_ANIMATION_DURATION, + LEAVE_BEHIND_ANIMATIONS_DELAY, 0L) + ); + return animation; + } + + private ObjectAnimator buildAnimator(IntProperty property, + int finalValueWhenAnimatingToLeaveBehind, int finalValueWhenAnimatingToAppIcons, + long duration, long delayWhenAnimatingToLeaveBehind, + long delayWhenAnimatingToAppIcons) { + final ObjectAnimator animator = ObjectAnimator.ofInt(this, property, + mIsActive ? finalValueWhenAnimatingToLeaveBehind + : finalValueWhenAnimatingToAppIcons); + applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind, + delayWhenAnimatingToAppIcons); + return animator; + } + + private ObjectAnimator buildAnimator(FloatProperty property, + float finalValueWhenAnimatingToLeaveBehind, float finalValueWhenAnimatingToAppIcons, + long duration, long delayWhenAnimatingToLeaveBehind, + long delayWhenAnimatingToAppIcons) { + final ObjectAnimator animator = ObjectAnimator.ofFloat(this, property, + mIsActive ? finalValueWhenAnimatingToLeaveBehind + : finalValueWhenAnimatingToAppIcons); + applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind, + delayWhenAnimatingToAppIcons); + return animator; + } + + private void applyTiming(ObjectAnimator animator, long duration, + long delayWhenAnimatingToLeaveBehind, + long delayWhenAnimatingToAppIcons) { + animator.setDuration(duration); + animator.setStartDelay( + mIsActive ? delayWhenAnimatingToLeaveBehind : delayWhenAnimatingToAppIcons); + } + + @Override + public MultiTranslateDelegate getTranslateDelegate() { + return mTranslateDelegate; + } + + @Override + public float getReorderBounceScale() { + return mScaleForReorderBounce; + } + + @Override + public void setReorderBounceScale(float scale) { + mScaleForReorderBounce = scale; + super.setScaleX(scale); + super.setScaleY(scale); + } + + private float getItemXOffset(float baseOffset, boolean isRtl, int itemIndex, int itemCount) { + // Item with index 1 is on the left in all cases. + if (itemIndex == 1) { + return (isRtl ? 1 : -1) * baseOffset; + } + + // First item is centered if total number of items shown is 3, on the right otherwise. + if (itemIndex == 0) { + if (itemCount == 3) { + return 0; + } + return (isRtl ? -1 : 1) * baseOffset; + } + + // Last item is on the right when there are more than 2 items (case which is already handled + // as `itemIndex == 1`). + if (itemIndex == itemCount - 1) { + return (isRtl ? -1 : 1) * baseOffset; + } + + return (isRtl ? 1 : -1) * baseOffset; + } + + private float getItemYOffset(float baseOffset, int itemIndex, int itemCount) { + // If icon contains two items, they are both centered vertically. + if (itemCount == 2) { + return 0; + } + // First half of items is on top, later half is on bottom. + return (itemIndex + 1 <= itemCount / 2 ? -1 : 1) * baseOffset; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt index 6c9cc642be..8d75eef314 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt @@ -24,18 +24,19 @@ import com.android.app.animation.Interpolators import com.android.launcher3.LauncherPrefs import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_TASKBAR_PINNED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_TASKBAR_UNPINNED import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_PINNED import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_UNPINNED import com.android.launcher3.taskbar.TaskbarDividerPopupView.Companion.createAndPopulate import java.io.PrintWriter +import kotlin.jvm.optionals.getOrNull /** Controls taskbar pinning through a popup view. */ -class TaskbarPinningController( - private val context: TaskbarActivityContext, - private val isInDesktopModeProvider: () -> Boolean, -) : TaskbarControllers.LoggableTaskbarController { +class TaskbarPinningController(private val context: TaskbarActivityContext) : + TaskbarControllers.LoggableTaskbarController { private lateinit var controllers: TaskbarControllers private lateinit var taskbarSharedState: TaskbarSharedState @@ -57,12 +58,26 @@ class TaskbarPinningController( if (!didPreferenceChange) { return } - val shouldPinTaskbar = - if (isInDesktopModeProvider()) { + + if ( + controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar( + context.displayId + ) + ) { + val shouldPinDesktopTaskbar = !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE) - } else { - !launcherPrefs.get(TASKBAR_PINNING) - } + val logEvent = + if (shouldPinDesktopTaskbar) { + LAUNCHER_DESKTOP_MODE_TASKBAR_PINNED + } else { + LAUNCHER_DESKTOP_MODE_TASKBAR_UNPINNED + } + statsLogManager.logger().log(logEvent) + launcherPrefs.put(TASKBAR_PINNING_IN_DESKTOP_MODE, shouldPinDesktopTaskbar) + return + } + + val shouldPinTaskbar = !launcherPrefs.get(TASKBAR_PINNING) val animateToValue = if (shouldPinTaskbar) { @@ -78,10 +93,10 @@ class TaskbarPinningController( } } - fun showPinningView(view: View) { + fun showPinningView(view: View, horizontalPosition: Float = -1f) { context.isTaskbarWindowFullscreen = true view.post { - val popupView = getPopupView(view) + val popupView = getPopupView(view, horizontalPosition) popupView.requestFocus() popupView.onCloseCallback = onCloseCallback context.onPopupVisibilityChanged(true) @@ -91,8 +106,8 @@ class TaskbarPinningController( } @VisibleForTesting - fun getPopupView(view: View): TaskbarDividerPopupView<*> { - return createAndPopulate(view, context) + fun getPopupView(view: View, horizontalPosition: Float = -1f): TaskbarDividerPopupView<*> { + return createAndPopulate(view, context, horizontalPosition) } @VisibleForTesting @@ -119,9 +134,13 @@ class TaskbarPinningController( dragLayerController.taskbarBackgroundProgress.animateToValue(animateToValue), taskbarViewController.taskbarIconTranslationYForPinning.animateToValue(animateToValue), taskbarViewController.taskbarIconScaleForPinning.animateToValue(animateToValue), - taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue) + taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue), ) - + controllers.bubbleControllers.getOrNull()?.bubbleBarViewController?.let { + // if bubble bar is not visible no need to add it`s animations + if (!it.isBubbleBarVisible) return@let + animatorSet.playTogether(it.bubbleBarPinning.animateToValue(animateToValue)) + } animatorSet.interpolator = Interpolators.EMPHASIZED return animatorSet } @@ -134,10 +153,14 @@ class TaskbarPinningController( @VisibleForTesting fun recreateTaskbarAndUpdatePinningValue() { updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false) - if (isInDesktopModeProvider()) { + if ( + controllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview( + context.displayId + ) + ) { launcherPrefs.put( TASKBAR_PINNING_IN_DESKTOP_MODE, - !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE) + !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE), ) } else { launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING)) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java index 2730be1b57..44832b52db 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -15,26 +15,31 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; +import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR; import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; import android.content.Intent; import android.content.pm.LauncherApps; import android.graphics.Point; +import android.os.UserHandle; import android.util.Pair; +import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; -import com.android.launcher3.dot.FolderDotInfo; -import com.android.launcher3.folder.Folder; -import com.android.launcher3.folder.FolderIcon; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationListener; @@ -44,19 +49,22 @@ import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.splitscreen.SplitShortcut; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.LauncherBindableItemsContainer; -import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; import com.android.launcher3.views.ActivityContext; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.LogUtils; +import com.android.quickstep.util.SingleTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Objects; -import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -69,16 +77,24 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba private static final SystemShortcut.Factory APP_INFO = SystemShortcut.AppInfo::new; + private static final SystemShortcut.Factory + BUBBLE = SystemShortcut.BubbleShortcut::new; + private final TaskbarActivityContext mContext; private final PopupDataProvider mPopupDataProvider; // Initialized in init. private TaskbarControllers mControllers; private boolean mAllowInitialSplitSelection; + private AppInfo[] mAppInfosList = AppInfo.EMPTY_ARRAY; + // Saves the ItemInfos in the hotseat without the predicted items. + private SparseArray mHotseatInfosList; + private ManageWindowsTaskbarShortcut mManageWindowsTaskbarShortcut; + public TaskbarPopupController(TaskbarActivityContext context) { mContext = context; - mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots); + mPopupDataProvider = new PopupDataProvider(mContext); } public void init(TaskbarControllers controllers) { @@ -100,41 +116,21 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy); } - public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) { - mAllowInitialSplitSelection = allowInitialSplitSelection; + /** Closes the multi-instance menu if it is enabled and currently open. */ + public void maybeCloseMultiInstanceMenu() { + if (Flags.enableMultiInstanceMenuTaskbar() && mManageWindowsTaskbarShortcut != null) { + mManageWindowsTaskbarShortcut.closeMultiInstanceMenu(); + cleanUpMultiInstanceMenuReference(); + } } - private void updateNotificationDots(Predicate updatedDots) { - final PackageUserKey packageUserKey = new PackageUserKey(null, null); - Predicate matcher = info -> !packageUserKey.updateFromItemInfo(info) - || updatedDots.test(packageUserKey); + /** Releases the reference to the Taskbar multi-instance menu */ + public void cleanUpMultiInstanceMenuReference() { + mManageWindowsTaskbarShortcut = null; + } - LauncherBindableItemsContainer.ItemOperator op = (info, v) -> { - if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) { - if (matcher.test(info)) { - ((BubbleTextView) v).applyDotState(info, true /* animate */); - } - } else if (info instanceof FolderInfo && v instanceof FolderIcon) { - FolderInfo fi = (FolderInfo) info; - if (fi.anyMatch(matcher)) { - FolderDotInfo folderDotInfo = new FolderDotInfo(); - for (ItemInfo si : fi.getContents()) { - folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si)); - } - ((FolderIcon) v).setDotInfo(folderDotInfo); - } - } - - // process all the shortcuts - return false; - }; - - mControllers.taskbarViewController.mapOverItems(op); - Folder folder = Folder.getOpen(mContext); - if (folder != null) { - folder.iterateOverItems(op); - } - mControllers.taskbarAllAppsController.updateNotificationDots(updatedDots); + public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) { + mAllowInitialSplitSelection = allowInitialSplitSelection; } /** @@ -148,22 +144,45 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba icon.clearFocus(); return null; } - ItemInfo item = (ItemInfo) icon.getTag(); - if (!ShortcutUtil.supportsShortcuts(item)) { + + ItemInfo itemInfo = null; + if (icon.getTag() instanceof ItemInfo item && ShortcutUtil.supportsShortcuts(item)) { + itemInfo = item; + } else if (PinToTaskbarShortcut.Companion.isPinningAppWithContextMenuEnabled(mContext) + && icon.getTag() instanceof SingleTask task) { + Task.TaskKey key = task.getTask().getKey(); + AppInfo appInfo = getApp( + new ComponentKey(key.getComponent(), UserHandle.of(key.userId))); + if (appInfo != null) { + WorkspaceItemInfo wif = appInfo.makeWorkspaceItem(icon.getContext()); + itemInfo = SingleTask.Companion.createTaskItemInfo(task, wif); + } + } + + if (itemInfo == null) { return null; } PopupContainerWithArrow container; - int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(item); + int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(itemInfo); // TODO(b/198438631): add support for INSTALL shortcut factory + final ItemInfo finalInfo = itemInfo; List systemShortcuts = getSystemShortcuts() - .map(s -> s.getShortcut(context, item, icon)) + .map(s -> s.getShortcut(context, finalInfo, icon)) .filter(Objects::nonNull) .collect(Collectors.toList()); + // TODO(b/375648361): Revisit to see if this can be implemented within getSystemShortcuts(). + if (PinToTaskbarShortcut.Companion.isPinningAppWithContextMenuEnabled(mContext)) { + SystemShortcut shortcut = createPinShortcut(context, itemInfo, icon); + if (shortcut != null) { + systemShortcuts.add(0, shortcut); + } + } + container = (PopupContainerWithArrow) context.getLayoutInflater().inflate( - R.layout.popup_container, context.getDragLayer(), false); - container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts); + R.layout.popup_container, context.getDragLayer(), false); + container.populateAndShowRows(icon, itemInfo, deepShortcutCount, systemShortcuts); // TODO (b/198438631): configure for taskbar/context container.setPopupItemDragHandler(new TaskbarPopupItemDragHandler()); @@ -181,11 +200,58 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba // Create a Stream of all applicable system shortcuts private Stream getSystemShortcuts() { - // append split options to APP_INFO shortcut, the order here will reflect in the popup - return Stream.concat( - Stream.of(APP_INFO), - mControllers.uiController.getSplitMenuOptions() - ); + // append split options to APP_INFO shortcut if not in Desktop Windowing mode, the order + // here will reflect in the popup + ArrayList shortcuts = new ArrayList<>(); + shortcuts.add(APP_INFO); + if (!mControllers.taskbarDesktopModeController + .isInDesktopModeAndNotInOverview(mContext.getDisplayId())) { + shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList()); + } + if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + shortcuts.add(BUBBLE); + } + + if (Flags.enableMultiInstanceMenuTaskbar() + && DesktopModeStatus.canEnterDesktopMode(mContext) + && !mControllers.taskbarStashController.isInOverview()) { + maybeCloseMultiInstanceMenu(); + shortcuts.addAll(getMultiInstanceMenuOptions().toList()); + } + return shortcuts.stream(); + } + + @Nullable + @VisibleForTesting + SystemShortcut createPinShortcut(BaseTaskbarContext target, ItemInfo itemInfo, + BubbleTextView originalView) { + // Predicted items use {@code HotseatPredictionController.PinPrediction} shortcut to pin. + if (itemInfo.isPredictedItem()) { + return null; + } + if (itemInfo.container == CONTAINER_HOTSEAT) { + return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false, + mHotseatInfosList); + } + + if (itemInfo.container == CONTAINER_ALL_APPS) { + // If the target ItemInfo is already pinned on taskbar. Show the unpin option instead. + for (int i = 0; i < mHotseatInfosList.size(); i++) { + if (Objects.equals(mHotseatInfosList.valueAt(i).getComponentKey(), + itemInfo.getComponentKey())) { + return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false, + mHotseatInfosList); + } + } + } + + if (mHotseatInfosList.size() + < mContext.getTaskbarSpecsEvaluator().getNumShownHotseatIcons()) { + return new PinToTaskbarShortcut<>(target, itemInfo, originalView, true, + mHotseatInfosList); + } + + return null; } @Override @@ -226,7 +292,10 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba // Move the icon to align with the center-top of the touch point Point iconShift = new Point(); iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; - iconShift.y = mIconLastTouchPos.y - mContext.getDeviceProfile().taskbarIconSize; + iconShift.y = mIconLastTouchPos.y + - mContext.getDeviceProfile() + .getTaskbarProfile() + .getIconSize(); ((TaskbarDragController) ActivityContext.lookupContext( v.getContext()).getDragController()).startDragOnLongClick(sv, iconShift); @@ -248,7 +317,83 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba originalView, position, mAllowInitialSplitSelection); } - /** + /** + * Set the list of AppInfos to be able to pull from later + */ + public void setApps(AppInfo[] apps) { + mAppInfosList = apps; + } + + /** + * Finds and returns an AppInfo object from a list, using its ComponentKey for identification. + * Based off of {@link com.android.launcher3.allapps.AllAppsStore#getApp(ComponentKey)} + * since we cannot access AllAppsStore from here. + */ + public AppInfo getApp(ComponentKey key) { + if (key == null) { + return null; + } + AppInfo tempInfo = new AppInfo(); + tempInfo.componentName = key.componentName; + tempInfo.user = key.user; + int index = Arrays.binarySearch(mAppInfosList, tempInfo, COMPONENT_KEY_COMPARATOR); + return index < 0 ? null : mAppInfosList[index]; + } + + public void setHotseatInfosList(SparseArray info) { + mHotseatInfosList = info; + } + + /** + * Returns a stream of Multi Instance menu options if an app supports it. + */ + Stream> getMultiInstanceMenuOptions() { + SystemShortcut.Factory f1 = createNewWindowShortcutFactory(); + SystemShortcut.Factory f2 = createManageWindowsShortcutFactory(); + return f1 != null ? Stream.of(f1, f2) : Stream.empty(); + } + + /** + * Creates a factory function representing a "New Window" menu item only if the calling app + * supports multi-instance. + * @return A factory function to be used in populating the long-press menu. + */ + SystemShortcut.Factory createNewWindowShortcutFactory() { + return (context, itemInfo, originalView) -> { + if (shouldShowMultiInstanceOptions(itemInfo)) { + return new NewWindowTaskbarShortcut<>(context, itemInfo, originalView); + } + return null; + }; + } + + /** + * Creates a factory function representing a "Manage Windows" menu item only if the calling app + * supports multi-instance. This menu item shows the open instances of the calling app. + * @return A factory function to be used in populating the long-press menu. + */ + public SystemShortcut.Factory createManageWindowsShortcutFactory() { + return (context, itemInfo, originalView) -> { + if (shouldShowMultiInstanceOptions(itemInfo)) { + mManageWindowsTaskbarShortcut = new ManageWindowsTaskbarShortcut<>( + context, itemInfo, originalView, mControllers); + return mManageWindowsTaskbarShortcut; + } + return null; + }; + } + + /** + * Determines whether to show multi-instance options for a given item. + */ + private boolean shouldShowMultiInstanceOptions(ItemInfo itemInfo) { + ComponentKey key = itemInfo.getComponentKey(); + AppInfo app = getApp(key); + return app != null && app.supportsMultiInstance() + && itemInfo.container != CONTAINER_ALL_APPS; + } + + /** * A single menu item ("Split left," "Split right," or "Split top") that executes a split * from the taskbar, as if the user performed a drag and drop split. * Includes an onClick method that initiates the actual split. diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt index b31b7c742a..f4d0f15501 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt @@ -15,18 +15,27 @@ */ package com.android.launcher3.taskbar -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration +import android.content.Context +import android.util.Log +import android.window.DesktopExperienceFlags +import android.window.DesktopModeFlags import androidx.annotation.VisibleForTesting +import com.android.launcher3.BubbleTextView.RunningAppState +import com.android.launcher3.Flags import com.android.launcher3.Flags.enableRecentsInTaskbar -import com.android.launcher3.model.data.AppInfo import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.TaskItemInfo import com.android.launcher3.model.data.WorkspaceItemInfo -import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.PinToTaskbarShortcut.Companion.isPinningAppWithContextMenuEnabled import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController +import com.android.launcher3.util.CancellableTask +import com.android.quickstep.RecentsFilterState import com.android.quickstep.RecentsModel -import com.android.window.flags2.Flags.enableDesktopWindowingMode -import com.android.window.flags2.Flags.enableDesktopWindowingTaskbarRunningApps +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import java.io.PrintWriter /** @@ -35,152 +44,463 @@ import java.io.PrintWriter * - When in Desktop Mode: show the currently running (open) Tasks */ class TaskbarRecentAppsController( + private val context: Context, private val recentsModel: RecentsModel, - // Pass a provider here instead of the actual DesktopVisibilityController instance since that - // instance might not be available when this constructor is called. - private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?, ) : LoggableTaskbarController { - // TODO(b/335401172): unify DesktopMode checks in Launcher. - val canShowRunningApps = - enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps() + var canShowRunningApps = + DesktopModeStatus.canEnterDesktopMode(context) && + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue + @VisibleForTesting + set(isEnabledFromTest) { + field = isEnabledFromTest + if (!field && !canShowRecentApps) { + recentsModel.unregisterRecentTasksChangedListener(recentTasksChangedListener) + } + } + + val enableRecentTasksThrottle = + DesktopExperienceFlags.ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX.isTrue // TODO(b/343532825): Add a setting to disable Recents even when the flag is on. - var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps + var canShowRecentApps = enableRecentsInTaskbar() @VisibleForTesting - set(isEnabledFromTest){ + set(isEnabledFromTest) { field = isEnabledFromTest + if (!field && !canShowRunningApps) { + recentsModel.unregisterRecentTasksChangedListener(recentTasksChangedListener) + } } // Initialized in init. private lateinit var controllers: TaskbarControllers - private var apps: Array? = null - private var allRunningDesktopAppInfos: List? = null - private var allMinimizedDesktopAppInfos: List? = null + var shownHotseatItems: List = emptyList() + private set - private val desktopVisibilityController: DesktopVisibilityController? - get() = desktopVisibilityControllerProvider() + private var allRecentTasks: List = emptyList() + private var desktopTasks: List = emptyList() + // Keeps track of the order in which running tasks appear. + private var orderedRunningTaskIds = emptyList() + var shownTasks: List = emptyList() + private set - private val isInDesktopMode: Boolean - get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false + val shownTaskIds: List + get() = shownTasks.flatMap { shownTask -> shownTask.tasks }.map { it.key.id } - val runningApps: Set + /** + * The task-state of an app, i.e. whether the app has a task and what state that task is in. + * + * @property taskId The ID of the task if one exists (i.e. if the state is RUNNING or + * MINIMIZED), null otherwise (NOT_RUNNING). + */ + data class TaskState(val runningAppState: RunningAppState, val taskId: Int? = null) + + /** + * Returns the state of the most active Desktop task represented by the given [ItemInfo]. + * + * If there are several tasks represented by the same [ItemInfo] we return the most active one, + * i.e. we return [DesktopAppState.RUNNING] over [DesktopAppState.MINIMIZED], and + * [DesktopAppState.MINIMIZED] over [DesktopAppState.NOT_RUNNING]. + */ + fun getDesktopItemState(itemInfo: ItemInfo?): TaskState { + val packageName = + itemInfo?.getTargetPackage() ?: return TaskState(RunningAppState.NOT_RUNNING) + return getDesktopTaskState(packageName, itemInfo.user.identifier) + } + + private fun getDesktopTaskState(packageName: String, userId: Int): TaskState { + if (desktopTasks.isEmpty()) { + return TaskState(RunningAppState.NOT_RUNNING) + } + val appTasks = + desktopTasks.filter { task -> + packageName == task.key.packageName && task.key.userId == userId + } + val runningTask = appTasks.find { getRunningAppState(it.key.id) == RunningAppState.RUNNING } + if (runningTask != null) { + return TaskState(RunningAppState.RUNNING, runningTask.key.id) + } + val minimizedTask = + appTasks.find { getRunningAppState(it.key.id) == RunningAppState.MINIMIZED } + if (minimizedTask != null) { + return TaskState(RunningAppState.MINIMIZED, taskId = minimizedTask.key.id) + } + return TaskState(RunningAppState.NOT_RUNNING) + } + + /** Get the [RunningAppState] for the given task. */ + fun getRunningAppState(taskId: Int): RunningAppState { + return when (taskId) { + in minimizedTaskIds -> RunningAppState.MINIMIZED + in runningTaskIds -> RunningAppState.RUNNING + else -> RunningAppState.NOT_RUNNING + } + } + + /** + * Returns `true` if recents has the single task (i.e., fullscreen) represented by the given + * [itemInfo]. + */ + fun hasSingleTask(itemInfo: ItemInfo?): Boolean { + val packageName = itemInfo?.targetPackage ?: return false + return allRecentTasks.any { task -> + task is SingleTask && + packageName == task.task.key.packageName && + task.task.key.userId == itemInfo.user.identifier + } + } + + @VisibleForTesting + val runningTaskIds: Set + /** + * Returns the task IDs of apps that should be indicated as "running" to the user. + * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet(). + */ get() { - if (!isEnabled || !isInDesktopMode) { + if ( + !canShowRunningApps || + !controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar() + ) { return emptySet() } - return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet() + return desktopTasks.map { it.key.id }.toSet() } - val minimizedApps: Set + @VisibleForTesting + val minimizedTaskIds: Set + /** + * Returns the task IDs for the tasks that should be indicated as "minimized" to the user. + */ get() { - if (!isInDesktopMode) { + if ( + !canShowRunningApps || + !controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar() + ) { return emptySet() } - return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() - ?: emptySet() + return desktopTasks.filter { !it.isVisible }.map { task -> task.key.id }.toSet() } - fun init(taskbarControllers: TaskbarControllers) { + private val recentTasksChangedListener = + RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() } + + private val iconLoadRequests: MutableSet> = HashSet() + + // TODO(b/343291428): add TaskVisualsChangListener as well (for calendar/clock?) + + // Used to keep track of the last requested task list ID, so that we do not request to load the + // tasks again if we have already requested it and the task list has not changed + private var taskListChangeId = -1 + + // Whether we're currently loading recents tasks + private var loadingRecentsTasks = false + // Whether we need to reload recents tasks when the current loading operation is finished + private var needsRecentsTasksReload = false + // Whether we've loaded recents tasks at least once + private var recentTasksLoaded = false + + fun init(taskbarControllers: TaskbarControllers, previousShownTasks: List) { controllers = taskbarControllers + if (previousShownTasks.isNotEmpty()) { + shownTasks = previousShownTasks + fetchIcons() + } + if (canShowRunningApps || canShowRecentApps) { + recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener) + controllers.runAfterInit { reloadRecentTasksIfNeeded() } + } } fun onDestroy() { - apps = null - } - - /** Stores the current [AppInfo] instances, no-op except in desktop environment. */ - fun setApps(apps: Array?) { - this.apps = apps + controllers.sharedState?.recentTasksBeforeTaskbarRecreate?.clear() + if (shownTasks.isNotEmpty()) { + controllers.sharedState?.recentTasksBeforeTaskbarRecreate?.addAll(shownTasks) + } + recentsModel.unregisterRecentTasksChangedListener(recentTasksChangedListener) + iconLoadRequests.forEach { it.cancel() } + iconLoadRequests.clear() } /** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */ - // TODO(next CL): add new section of Tasks instead of changing Hotseat items fun updateHotseatItemInfos(hotseatItems: Array): Array { - if (!isEnabled || !isInDesktopMode) { + // Ignore predicted apps - we show running or recent apps instead. + val showDesktopTasks = + controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar() + val removePredictions = + (showDesktopTasks && canShowRunningApps) || (!showDesktopTasks && canShowRecentApps) + if (!removePredictions) { + shownHotseatItems = hotseatItems.filterNotNull() + onRecentsOrHotseatChanged() return hotseatItems } - val newHotseatItemInfos = + shownHotseatItems = hotseatItems .filterNotNull() - // Ignore predicted apps - we show running apps instead .filter { itemInfo -> !itemInfo.isPredictedItem } .toMutableList() - val runningDesktopAppInfos = - allRunningDesktopAppInfos?.let { - getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList()) + + if (showDesktopTasks && canShowRunningApps) { + shownHotseatItems = + updateHotseatItemsFromRunningTasks( + getOrderedAndWrappedDesktopTasks(), + shownHotseatItems, + ) + } + + if (recentTasksLoaded) { + onRecentsOrHotseatChanged() + } + + return shownHotseatItems.toTypedArray() + } + + private fun getOrderedAndWrappedDesktopTasks(): List { + // We wrap each task in the Desktop as a `SingleTask`. + val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index } + val sortedTasks = desktopTasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] }) + return sortedTasks.map { SingleTask(it) } + } + + private fun reloadRecentTasksIfNeeded() { + if (recentsModel.isTaskListValid(taskListChangeId)) return + if (enableRecentTasksThrottle && loadingRecentsTasks) { + Log.v(TAG, "reloadRecentTasksIfNeeded: tried to reload while loading recents tasks") + needsRecentsTasksReload = true + return + } + Log.v(TAG, "reloadRecentTasksIfNeeded: load recents tasks") + // Only indicate that recents tasks are loading if the enableRecentTasksThrottle flag is on + loadingRecentsTasks = enableRecentTasksThrottle + taskListChangeId = + recentsModel.getTasks(RecentsFilterState.EMPTY_FILTER) { tasks -> + loadingRecentsTasks = false + recentTasksLoaded = true + allRecentTasks = tasks + val oldRunningTaskdIds = runningTaskIds + val oldMinimizedTaskIds = minimizedTaskIds + desktopTasks = allRecentTasks.filterIsInstance().flatMap { it.tasks } + val runningTasksChanged = oldRunningTaskdIds != runningTaskIds + val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds + + if ( + (onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged) && + !controllers.taskbarDesktopModeController.isLauncherAnimationRunning + ) { + controllers.taskbarViewController.commitRunningAppsToUI() + } + if (needsRecentsTasksReload) { + Log.v(TAG, "reloadRecentTasksIfNeeded: reload recents tasks") + needsRecentsTasksReload = false + reloadRecentTasksIfNeeded() + } } - if (runningDesktopAppInfos != null) { - newHotseatItemInfos.addAll(runningDesktopAppInfos) - } - return newHotseatItemInfos.toTypedArray() } - private fun getRunningDesktopAppInfosExceptHotseatApps( - allRunningDesktopAppInfos: List, - hotseatItems: List - ): List { - val hotseatPackages = hotseatItems.map { it.targetPackage } - return allRunningDesktopAppInfos - .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) } - .map { WorkspaceItemInfo(it) } + /** + * Updates [shownTasks] when Recents or Hotseat changes. + * + * @return Whether [shownTasks] changed. + */ + private fun onRecentsOrHotseatChanged(): Boolean { + val oldShownTasks = shownTasks + orderedRunningTaskIds = updateOrderedRunningTaskIds() + shownTasks = + if (controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()) { + computeShownRunningTasks() + } else { + computeShownRecentTasks() + } + val shownTasksChanged = oldShownTasks != shownTasks + if (!shownTasksChanged) { + return shownTasksChanged + } + fetchIcons() + return shownTasksChanged } - private fun getDesktopRunningTasks(): List = - recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo -> - taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM + private fun fetchIcons() { + for (groupTask in shownTasks) { + for (task in groupTask.tasks) { + val cancellableTask = + recentsModel.iconCache.getIconInBackground(task) { + icon, + contentDescription, + title -> + task.icon = icon + task.titleDescription = contentDescription + task.title = title + controllers.taskbarViewController.onTaskUpdated(task) + } + if (cancellableTask != null) { + iconLoadRequests.add(cancellableTask) + } + } } + } - // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos - private fun getAppInfosFromRunningTasks(tasks: List): List { - // Early return if apps is empty, since we then have no AppInfo to compare to - if (apps == null) { + private fun updateOrderedRunningTaskIds(): MutableList { + val desktopTasksAsList = getOrderedAndWrappedDesktopTasks().map { it.task } + val desktopTaskIds = desktopTasksAsList.map { it.key.id } + var newOrder = + orderedRunningTaskIds + .filter { it in desktopTaskIds } // Only keep the tasks that are still running + .toMutableList() + // Add new tasks not already listed + newOrder.addAll(desktopTaskIds.filter { it !in newOrder }) + return newOrder + } + + /** + * Computes the list of running tasks to be shown in the recent apps section of the taskbar in + * desktop mode, taking into account deduplication against hotseat items and existing tasks. + */ + private fun computeShownRunningTasks(): List { + if (!canShowRunningApps) { return emptyList() } - val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull() - return packageNames - .map { packageName -> apps?.find { app -> packageName == app.targetPackage } } - .filterNotNull() + + val desktopTasks = getOrderedAndWrappedDesktopTasks() + + val newShownTasks = + if (Flags.enableMultiInstanceMenuTaskbar()) { + val deduplicatedDesktopTasks = + desktopTasks.distinctBy { Pair(it.task.key.packageName, it.task.key.userId) } + val activityContext = controllers.taskbarActivityContext + + shownTasks + .filter { + it is SingleTask && + it.task.key.id in deduplicatedDesktopTasks.map { it.task.key.id } && + (!isPinningAppWithContextMenuEnabled(activityContext) || + shownHotseatItems.none { hotseatItem -> + it.containsPackage( + hotseatItem.targetPackage, + hotseatItem.user.identifier, + ) + }) + } + .toMutableList() + .apply { + addAll( + deduplicatedDesktopTasks.filter { currentTask -> + val currentTaskKey = currentTask.task.key + currentTaskKey.id !in shownTaskIds && + shownHotseatItems.none { hotseatItem -> + currentTask.containsPackage( + hotseatItem.targetPackage, + hotseatItem.user.identifier, + ) + } + } + ) + } + } else { + val desktopTaskIds = desktopTasks.map { it.task.key.id } + val shownHotseatItemTaskIds = + shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId } + + shownTasks + .filter { it is SingleTask && it.task.key.id in desktopTaskIds } + .toMutableList() + .apply { + addAll( + desktopTasks.filter { desktopTask -> + desktopTask.task.key.id !in shownTaskIds + } + ) + removeAll { it is SingleTask && it.task.key.id in shownHotseatItemTaskIds } + } + } + + return newShownTasks } - /** Called to update the list of currently running apps, no-op except in desktop environment. */ - fun updateRunningApps() { - if (!isEnabled || !isInDesktopMode) { - return controllers.taskbarViewController.commitRunningAppsToUI() + private fun computeShownRecentTasks(): List { + if (!canShowRecentApps || allRecentTasks.isEmpty()) { + return emptyList() } - val runningTasks = getDesktopRunningTasks() - val runningAppInfo = getAppInfosFromRunningTasks(runningTasks) - allRunningDesktopAppInfos = runningAppInfo - updateMinimizedApps(runningTasks, runningAppInfo) - controllers.taskbarViewController.commitRunningAppsToUI() + // Remove the current task. + val allRecentTasks = allRecentTasks.subList(0, allRecentTasks.size - 1) + var shownTasks = dedupeHotseatTasks(allRecentTasks, shownHotseatItems) + if (shownTasks.size > MAX_RECENT_TASKS) { + // Remove any tasks older than MAX_RECENT_TASKS. + shownTasks = shownTasks.subList(shownTasks.size - MAX_RECENT_TASKS, shownTasks.size) + } + return shownTasks } - private fun updateMinimizedApps( - runningTasks: List, - runningAppInfo: List, - ) { - val allRunningAppTasks = - runningAppInfo - .mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } } - .associate { (appInfo, targetPackage) -> - appInfo to - runningTasks - .filter { it.realActivity?.packageName == targetPackage } - .map { it.taskId } + private fun dedupeHotseatTasks( + groupTasks: List, + shownHotseatItems: List, + ): List { + // TODO: b/393476333 - Check the behavior of the Taskbar recents section when empty desks + // become supported. + return if (Flags.enableMultiInstanceMenuTaskbar()) { + groupTasks.filter { groupTask -> + // Keep tasks that are group tasks or unique package name/user combinations + when (groupTask) { + is SingleTask -> + shownHotseatItems.none { + groupTask.containsPackage(it.targetPackage, it.user.identifier) + } + + else -> true } - val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible } - allMinimizedDesktopAppInfos = - allRunningAppTasks - .filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } } - .keys - .toList() + } + } else { + val hotseatPackages = shownHotseatItems.map { it.targetPackage } + groupTasks.filter { groupTask -> + when (groupTask) { + is SingleTask -> hotseatPackages.none { groupTask.containsPackage(it) } + + else -> true + } + } + } } + /** + * Returns the hotseat items updated so that any item that points to a package+user with a + * running task also references that task. + */ + private fun updateHotseatItemsFromRunningTasks( + groupTasks: List, + shownHotseatItems: List, + ): List = + shownHotseatItems.map { itemInfo -> + if (itemInfo is TaskItemInfo) { + itemInfo + } else { + val foundTask = + groupTasks + .flatMap { it.tasks } + .find { task -> + task.key.packageName == itemInfo.targetPackage && + task.key.userId == itemInfo.user.identifier + } ?: return@map itemInfo + TaskItemInfo(foundTask.key.id, itemInfo as WorkspaceItemInfo) + } + } + override fun dumpLogs(prefix: String, pw: PrintWriter) { pw.println("$prefix TaskbarRecentAppsController:") - pw.println("$prefix\tisEnabled=$isEnabled") pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps") - // TODO(next CL): add more logs + pw.println("$prefix\tcanShowRecentApps=$canShowRecentApps") + pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}") + pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}") + pw.println("$prefix\tdesktopTask=${desktopTasks.map { it.key.packageName }}") + pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}") + pw.println("$prefix\trunningTaskIds=$runningTaskIds") + pw.println("$prefix\tminimizedTaskIds=$minimizedTaskIds") + } + + private val GroupTask.packageNames: List + get() = tasks.map { task -> task.key.packageName } + + private companion object { + private val TAG = "TaskbarRecentAppsController" + + const val MAX_RECENT_TASKS = 2 } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationController.kt new file mode 100644 index 0000000000..4e690f192b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationController.kt @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.animation.AnimatorSet +import android.animation.ArgbEvaluator +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.Color.TRANSPARENT +import android.util.FloatProperty +import android.util.IntProperty +import androidx.annotation.ColorInt +import androidx.annotation.Px +import androidx.annotation.UiThread +import androidx.core.animation.doOnEnd +import com.android.app.animation.Interpolators.EMPHASIZED +import com.android.app.animation.Interpolators.LINEAR +import com.android.internal.dynamicanimation.animation.FloatValueHolder +import com.android.internal.dynamicanimation.animation.SpringAnimation +import com.android.internal.dynamicanimation.animation.SpringForce +import com.android.internal.dynamicanimation.animation.SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY +import com.android.internal.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY +import com.android.launcher3.BubbleTextView +import com.android.launcher3.BubbleTextView.RunningAppState +import com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED +import com.android.launcher3.BubbleTextView.RunningAppState.NOT_RUNNING +import com.android.launcher3.BubbleTextView.RunningAppState.RUNNING +import com.android.launcher3.R +import com.android.launcher3.Utilities as LauncherUtilities +import com.android.launcher3.model.data.TaskItemInfo +import com.android.launcher3.taskbar.TaskbarViewController.TRANSITION_DEFAULT_DURATION +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_APP_RUNNING_STATE_ANIM + +private const val SPRING_START = 0f +private const val SPRING_END = 100f + +private val SPRING_NO_BOUNCY = + SpringForce(SPRING_END).apply { + dampingRatio = DAMPING_RATIO_NO_BOUNCY + stiffness = 3800f + } +private val SPRING_MEDIUM_BOUNCY = + SpringForce(SPRING_END).apply { + dampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY + stiffness = 400f + } + +private val TRANSLATE_Y_PROPERTY = + object : FloatProperty("runningStateTranslateY") { + private val BubbleTextView.runningStateTranslateYProp: MultiPropertyFactory<*>.MultiProperty + get() = translateDelegate.getTranslationY(INDEX_TASKBAR_APP_RUNNING_STATE_ANIM) + + override fun get(btv: BubbleTextView): Float = btv.runningStateTranslateYProp.value + + override fun setValue(btv: BubbleTextView, translateY: Float) { + btv.runningStateTranslateYProp.value = translateY + btv.invalidate() + } + } + +private val LINE_COLOR_PROPERTY = + object : IntProperty("lineIndicatorColor") { + override fun get(btv: BubbleTextView): Int = btv.lineIndicatorColor + + override fun setValue(btv: BubbleTextView, color: Int) { + btv.lineIndicatorColor = color + } + } + +private val LINE_WIDTH_PROPERTY = + object : FloatProperty("lineIndicatorWidth") { + override fun get(btv: BubbleTextView): Float = btv.lineIndicatorWidth + + override fun setValue(btv: BubbleTextView, width: Float) { + btv.lineIndicatorWidth = width + } + } + +/** Manages Taskbar [BubbleTextView] animations from changes in [RunningAppState]. */ +class TaskbarRunningAppStateAnimationController(context: Context) { + + /** Animating [BubbleTextView] instances, where invoking each value cancels the animation. */ + private val runningAnimations = mutableMapOf Unit>() + + private val argbEvaluator = ArgbEvaluator() + private val runningLineColor = + context.resources.getColor(R.color.taskbar_running_app_indicator_color, context.theme) + private val minimizedLineColor = + context.resources.getColor(R.color.taskbar_minimized_app_indicator_color, context.theme) + + private val runningLineWidth = + context.resources + .getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width) + .toFloat() + private val minimizedLineWidth = + context.resources + .getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width) + .toFloat() + + private val appTranslateYSpring = + context.resources.getDimensionPixelSize(R.dimen.taskbar_app_translate_y_spring).toFloat() + private val lineWidthSpring = + context.resources + .getDimensionPixelSize(R.dimen.taskbar_line_indicator_width_spring) + .toFloat() + + // Copies the keys to avoid concurrent modification due to value callbacks modifying the map. + fun onDestroy() = runningAnimations.keys.toList().forEach { cancelAnimation(it) } + + @UiThread + fun updateRunningState(btv: BubbleTextView, runningState: RunningAppState, animate: Boolean) { + val prevRunningState = btv.runningAppState ?: NOT_RUNNING + if (runningState == prevRunningState) return + + cancelAnimation(btv) + btv.runningAppState = runningState + if (!animate) return applyRunningState(btv) + + val isPinnedApp = btv.tag is TaskItemInfo + if ( + (prevRunningState == RUNNING && runningState == MINIMIZED) || + (prevRunningState == MINIMIZED && runningState == RUNNING) || + (isPinnedApp && runningState == RUNNING) + ) { + return startAppBounceAnimation(btv) + } + + // Otherwise just animate line width and color. + AnimatorSet().run { + if (runningState == RUNNING) { + // New unpinned app - delay animation until icon is mostly scaled in. + startDelay = UNPINNED_APP_LINE_ANIM_DELAY + } + + duration = LINE_ANIM_DURATION + interpolator = EMPHASIZED + + playTogether( + ObjectAnimator.ofFloat(btv, LINE_WIDTH_PROPERTY, runningState.lineWidth), + ObjectAnimator.ofArgb(btv, LINE_COLOR_PROPERTY, runningState.lineColor), + ) + + doOnEnd { + runningAnimations.remove(btv) + applyRunningState(btv) + } + + runningAnimations[btv] = this::cancel + start() + } + } + + fun isAnimationRunning(btv: BubbleTextView): Boolean = runningAnimations.containsKey(btv) + + private fun cancelAnimation(btv: BubbleTextView) = runningAnimations[btv]?.invoke() + + private fun startAppBounceAnimation(btv: BubbleTextView) { + val isMinimized = btv.runningAppState == MINIMIZED + val translateYSpring = if (isMinimized) appTranslateYSpring else -appTranslateYSpring + val prevLineWidth = btv.lineIndicatorWidth + val prevLineColor = btv.lineIndicatorColor + + val translateYProp = + btv.translateDelegate.getTranslationY(INDEX_TASKBAR_APP_RUNNING_STATE_ANIM) + fun updateTranslateY(value: Float) { + translateYProp.value = value + btv.invalidate() + } + + SpringAnimation(FloatValueHolder()).run { + spring = SPRING_NO_BOUNCY + addUpdateListener { _, v, _ -> + updateTranslateY(mapValue(v, 0f, translateYSpring)) + if (isMinimized) { + btv.lineIndicatorWidth = mapValue(v, prevLineWidth, lineWidthSpring) + } + } + + addEndListener { _, canceled, _, _ -> + runningAnimations.remove(btv) + if (canceled) return@addEndListener applyRunningState(btv) + + val startLineWidth = if (isMinimized) lineWidthSpring else prevLineWidth + val endLineWidth = btv.runningAppState.lineWidth + val endLineColor = btv.runningAppState.lineColor + + val springs = + listOf( + SpringAnimation(FloatValueHolder()).apply { + spring = SPRING_MEDIUM_BOUNCY + addUpdateListener { _, v, _ -> + updateTranslateY(mapValue(v, translateYSpring, 0f)) + btv.lineIndicatorWidth = mapValue(v, startLineWidth, endLineWidth) + } + }, + SpringAnimation(FloatValueHolder()).apply { + spring = SPRING_NO_BOUNCY + addUpdateListener { _, v, _ -> + btv.lineIndicatorColor = + argbEvaluator.evaluate( + v / SPRING_END, + prevLineColor, + endLineColor, + ) as Int + } + }, + ) + + runningAnimations[btv] = { for (s in springs) s.cancel() } + var runningSprings = springs.size + for (s in springs) { + s.addEndListener { _, canceled2, _, _ -> + if (--runningSprings == 0) { + runningAnimations.remove(btv) + if (canceled2) applyRunningState(btv) + } + } + s.start() + } + } + + runningAnimations[btv] = this::cancel + start() + } + } + + private fun applyRunningState(btv: BubbleTextView) { + btv.lineIndicatorWidth = btv.runningAppState.lineWidth + btv.lineIndicatorColor = btv.runningAppState.lineColor + TRANSLATE_Y_PROPERTY[btv] = 0f + } + + private fun mapValue(value: Float, min: Float, max: Float): Float { + return LauncherUtilities.mapToRange(value, SPRING_START, SPRING_END, min, max, LINEAR) + } + + @get:ColorInt + val RunningAppState.lineColor: Int + get() { + return when (this) { + NOT_RUNNING -> TRANSPARENT + RUNNING -> runningLineColor + MINIMIZED -> minimizedLineColor + } + } + + @get:Px + val RunningAppState.lineWidth: Float + get() { + return when (this) { + NOT_RUNNING -> 0f + RUNNING -> runningLineWidth + MINIMIZED -> minimizedLineWidth + } + } + + companion object { + const val LINE_ANIM_DURATION = 100L + const val UNPINNED_APP_LINE_ANIM_DELAY = TRANSITION_DEFAULT_DURATION - LINE_ANIM_DURATION + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java index 48d2bc2ff7..bf73f02aab 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java @@ -20,15 +20,17 @@ import static android.view.View.VISIBLE; import static com.android.launcher3.taskbar.bubbles.BubbleBarController.isBubbleBarEnabled; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED; -import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE; +import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import android.animation.ObjectAnimator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.anim.AnimatedFloat; -import com.android.launcher3.util.DisplayController; +import com.android.launcher3.taskbar.bubbles.BubbleControllers; import com.android.quickstep.SystemUiProxy; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -65,6 +67,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa */ public void init(TaskbarControllers controllers) { mControllers = controllers; + onTaskbarVisibilityChanged(mControllers.taskbarViewController.getTaskbarVisibility()); } /** @@ -75,7 +78,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa public void onTaskbarVisibilityChanged(int visibility) { mTaskbarVisible = visibility == VISIBLE; if (shouldShowScrim()) { - showScrim(true, getScrimAlpha(), false /* skipAnim */); + showScrim(true, computeScrimAlpha(), false /* skipAnim */); } else if (mScrimView.getScrimAlpha() > 0f) { showScrim(false, 0, false /* skipAnim */); } @@ -85,26 +88,42 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa * Updates the scrim state based on the flags. */ public void updateStateForSysuiFlags(@SystemUiStateFlags long stateFlags, boolean skipAnim) { - if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) { + if (mActivity.isPhoneMode()) { + // There is no scrim for the bar in the phone mode. + return; + } + boolean isTransient = mActivity.isTransientTaskbar(); + if (isBubbleBarEnabled() && isTransient) { // These scrims aren't used if bubble bar & transient taskbar are active. return; } mSysUiStateFlags = stateFlags; - showScrim(shouldShowScrim(), getScrimAlpha(), skipAnim); + showScrim(shouldShowScrim(), computeScrimAlpha(), skipAnim); } private boolean shouldShowScrim() { final boolean bubblesExpanded = (mSysUiStateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0; boolean isShadeVisible = (mSysUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0; + BubbleControllers bubbleControllers = mActivity.getBubbleControllers(); + boolean isBubbleControllersPresented = bubbleControllers != null; + // when the taskbar is in persistent mode, we hide the task bar icons on bubble bar expand, + // which makes the taskbar invisible, so need to check if the bubble bar is not on home + // to show the scrim view + boolean showScrimForBubbles = bubblesExpanded + && !mTaskbarVisible + && isBubbleControllersPresented + && !mActivity.isTransientTaskbar() + && !bubbleControllers.bubbleStashController.isBubblesShowingOnHome(); return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible() && !isShadeVisible && !mControllers.taskbarStashController.isStashed() - && mTaskbarVisible; + && (mTaskbarVisible || showScrimForBubbles) + && !mControllers.taskbarStashController.isHiddenForBubbles(); } - private float getScrimAlpha() { - final boolean isPersistentTaskBarVisible = - mTaskbarVisible && !DisplayController.isTransientTaskbar(mScrimView.getContext()); + private float computeScrimAlpha() { + boolean isTransient = mActivity.isTransientTaskbar(); + final boolean isPersistentTaskBarVisible = mTaskbarVisible && !isTransient; final boolean manageMenuExpanded = (mSysUiStateFlags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0; if (isPersistentTaskBarVisible && manageMenuExpanded) { @@ -123,7 +142,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa mScrimView.setOnClickListener(showScrim ? (view) -> onClick() : null); mScrimView.setClickable(showScrim); if (skipAnim) { - mScrimView.setScrimAlpha(alpha); + mScrimAlpha.updateValue(alpha); } else { ObjectAnimator anim = mScrimAlpha.animateToValue(showScrim ? alpha : 0); anim.setInterpolator(showScrim ? SCRIM_ALPHA_IN : SCRIM_ALPHA_OUT); @@ -136,7 +155,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa } private void onClick() { - SystemUiProxy.INSTANCE.get(mActivity).onBackPressed(); + SystemUiProxy.INSTANCE.get(mActivity).onBackEvent(null); } @Override @@ -150,4 +169,14 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa pw.println(prefix + "\tmScrimAlpha.value=" + mScrimAlpha.value); } + + @VisibleForTesting + TaskbarScrimView getScrimView() { + return mScrimView; + } + + @VisibleForTesting + float getScrimAlpha() { + return mScrimAlpha.value; + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java index edaeb63381..89016c8f45 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java @@ -29,7 +29,13 @@ import android.os.Binder; import android.os.IBinder; import android.view.InsetsFrameProvider; +import com.android.quickstep.util.GroupTask; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleInfo; + +import java.util.ArrayList; +import java.util.List; /** * State shared across different taskbar instance @@ -43,6 +49,8 @@ public class TaskbarSharedState { // TaskbarManager#onSystemUiFlagsChanged @SystemUiStateFlags public long sysuiStateFlags; + // TaskBarStashController#init() + public boolean isTaskbarOnOverview; // TaskbarManager#disableNavBarElements() public int disableNavBarDisplayId; @@ -56,14 +64,46 @@ public class TaskbarSharedState { // TaskbarManager#onNavButtonsDarkIntensityChanged() public float navButtonsDarkIntensity; + // TaskbarManager#onTransitionModeUpdated() + public int barMode; + // TaskbarManager#onNavigationBarLumaSamplingEnabled() public int mLumaSamplingDisplayId = DEFAULT_DISPLAY; public boolean mIsLumaSamplingEnabled = true; public boolean setupUIVisible = false; + public boolean wallpaperVisible = false; + public boolean allAppsVisible = false; + public boolean bubbleBarExpanded = false; + + public boolean bubbleBarStashed = false; + + public String selectedBubbleKey; + + public BubbleBarLocation bubbleBarLocation; + + public List bubbleInfoItems; + + public List suppressedBubbleInfoItems; + + /** Returns whether there are a saved bubbles. */ + public boolean hasSavedBubbles() { + return bubbleInfoItems != null && !bubbleInfoItems.isEmpty(); + } + + /** Clears stored bubble bar data. */ + public void clearBubbleData() { + bubbleInfoItems = null; + selectedBubbleKey = null; + bubbleBarLocation = null; + bubbleBarExpanded = false; + bubbleBarStashed = false; + suppressedBubbleInfoItems = null; + } + // LauncherTaskbarUIController#mTaskbarInAppDisplayProgressMultiProp public float[] inAppDisplayProgressMultiPropValues = new float[DISPLAY_PROGRESS_COUNT]; @@ -97,5 +137,10 @@ public class TaskbarSharedState { // To track if taskbar was stashed / unstashed between configuration changes (which recreates // the task bar). - public Boolean taskbarWasStashedAuto = true; + public boolean taskbarWasStashedAuto = true; + + // should show corner radius on persistent taskbar when in desktop mode. + public boolean showCornerRadiusInDesktopMode = false; + + public List recentTasksBeforeTaskbarRecreate = new ArrayList<>(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java index 25db960ea3..94cff0ba1e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java @@ -22,6 +22,7 @@ import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventFo import android.content.Intent; import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; import android.util.Pair; import android.view.KeyEvent; import android.view.View; @@ -38,6 +39,7 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.ShortcutUtil; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.LogUtils; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import java.util.List; @@ -50,6 +52,7 @@ public class TaskbarShortcutMenuAccessibilityDelegate public static final int MOVE_TO_TOP_OR_LEFT = R.id.action_move_to_top_or_left; public static final int MOVE_TO_BOTTOM_OR_RIGHT = R.id.action_move_to_bottom_or_right; + public static final int CREATE_APPLICATION_BUBBLE = R.id.action_create_application_bubble; private final LauncherApps mLauncherApps; private final StatsLogManager mStatsLogManager; @@ -67,6 +70,9 @@ public class TaskbarShortcutMenuAccessibilityDelegate MOVE_TO_BOTTOM_OR_RIGHT, R.string.move_drop_target_bottom_or_right, KeyEvent.KEYCODE_R)); + mActions.put(CREATE_APPLICATION_BUBBLE, new LauncherAction( + CREATE_APPLICATION_BUBBLE, R.string.open_app_as_a_bubble, + KeyEvent.KEYCODE_L)); } @Override @@ -76,11 +82,27 @@ public class TaskbarShortcutMenuAccessibilityDelegate } out.add(mActions.get(MOVE_TO_TOP_OR_LEFT)); out.add(mActions.get(MOVE_TO_BOTTOM_OR_RIGHT)); + if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + out.add(mActions.get(CREATE_APPLICATION_BUBBLE)); + } } @Override protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) { - if (item instanceof ItemInfoWithIcon + if (action == DEEP_SHORTCUTS) { + mContext.showPopupMenuForIcon((BubbleTextView) host); + return true; + } else if (action == CREATE_APPLICATION_BUBBLE) { + if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT + && item instanceof WorkspaceItemInfo) { + ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) item).getDeepShortcutInfo(); + SystemUiProxy.INSTANCE.get(mContext).showShortcutBubble(shortcutInfo); + return true; + } else if (item.getIntent() != null && item.getIntent().getPackage() != null) { + SystemUiProxy.INSTANCE.get(mContext).showAppBubble(item.getIntent(), item.user); + return true; + } + } else if (item instanceof ItemInfoWithIcon && (action == MOVE_TO_TOP_OR_LEFT || action == MOVE_TO_BOTTOM_OR_RIGHT)) { ItemInfoWithIcon info = (ItemInfoWithIcon) item; int side = action == MOVE_TO_TOP_OR_LEFT @@ -111,10 +133,6 @@ public class TaskbarShortcutMenuAccessibilityDelegate item.user.getIdentifier(), new Intent(), side, null, instanceIds.first); } - return true; - } else if (action == DEEP_SHORTCUTS) { - mContext.showPopupMenuForIcon((BubbleTextView) host); - return true; } return false; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java index f87c21ece0..fa35a03216 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSpringOnStashController.java @@ -27,7 +27,6 @@ import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.SpringAnimationBuilder; import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController; -import com.android.launcher3.util.DisplayController; import java.io.PrintWriter; @@ -47,7 +46,7 @@ public class TaskbarSpringOnStashController implements LoggableTaskbarController public TaskbarSpringOnStashController(TaskbarActivityContext context) { mContext = context; - mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext); + mIsTransientTaskbar = context.isTransientTaskbar(); mStartVelocityPxPerS = context.getResources() .getDimension(R.dimen.transient_taskbar_stash_spring_velocity_dp_per_s); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index 7ff887c7d2..6fa55799c3 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -23,16 +23,20 @@ import static com.android.app.animation.Interpolators.INSTANT; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.internal.jank.InteractionJankMonitor.Configuration; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; +import static com.android.launcher3.Flags.syncAppLaunchWithTaskbarStash; +import static com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW; +import static com.android.launcher3.taskbar.TaskbarActivityContext.ENABLE_TASKBAR_BEHIND_SHADE; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.FlagDebugUtils.appendFlag; import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange; import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR; +import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; @@ -58,19 +62,19 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.launcher3.Alarm; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorListeners; -import com.android.launcher3.statehandlers.DesktopVisibilityController; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; -import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.SystemUiFlagUtils; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; import java.util.StringJoiner; import java.util.function.LongPredicate; @@ -82,6 +86,11 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private static final String TAG = "TaskbarStashController"; private static final boolean DEBUG = false; + /** + * Def. value for @param shouldBubblesFollow in + * {@link #updateAndAnimateTransientTaskbar(boolean)} */ + public static boolean SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE = true; + public static final int FLAG_IN_APP = 1 << 0; public static final int FLAG_STASHED_IN_APP_SYSUI = 1 << 1; // shade open, ... public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 2; // setup wizard and AllSetActivity @@ -94,6 +103,14 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba public static final int FLAG_STASHED_SYSUI = 1 << 9; // app pinning,... public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ... public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview + // An internal no-op flag to determine whether we should delay the taskbar background animation + private static final int FLAG_DELAY_TASKBAR_BG_TAG = 1 << 12; + public static final int FLAG_STASHED_FOR_BUBBLES = 1 << 13; // show handle for stashed hotseat + public static final int FLAG_TASKBAR_HIDDEN = 1 << 14; // taskbar hidden during dream, etc... + // taskbar should always be stashed for bubble bar on phone + public static final int FLAG_STASHED_BUBBLE_BAR_ON_PHONE = 1 << 15; + + public static final int FLAG_IGNORE_IN_APP = 1 << 16; // used to sync with app launch animation // If any of these flags are enabled, isInApp should return true. private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP; @@ -115,26 +132,30 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba // If any of these flags are enabled, the taskbar must be stashed. private static final int FLAGS_FORCE_STASHED = FLAG_STASHED_SYSUI | FLAG_STASHED_DEVICE_LOCKED - | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN; + | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN + | FLAG_STASHED_FOR_BUBBLES | FLAG_STASHED_BUBBLE_BAR_ON_PHONE; /** * How long to stash/unstash when manually invoked via long press. * * Use {@link #getStashDuration()} to query duration */ - private static final long TASKBAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE; + @VisibleForTesting + static final long TASKBAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE; /** * How long to stash/unstash transient taskbar. * * Use {@link #getStashDuration()} to query duration. */ - private static final long TRANSIENT_TASKBAR_STASH_DURATION = 417; + @VisibleForTesting + static final long TRANSIENT_TASKBAR_STASH_DURATION = 417; /** * How long to stash/unstash when keyboard is appearing/disappearing. */ - private static final long TASKBAR_STASH_DURATION_FOR_IME = 80; + @VisibleForTesting + static final long TASKBAR_STASH_DURATION_FOR_IME = 80; /** * The scale TaskbarView animates to when being stashed. @@ -150,12 +171,12 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * How long to delay the icon/stash handle alpha. */ - private static final long TASKBAR_STASH_ALPHA_START_DELAY = 33; + public static final long TASKBAR_STASH_ALPHA_START_DELAY = 33; /** * How long the icon/stash handle alpha animation plays. */ - private static final long TASKBAR_STASH_ALPHA_DURATION = 50; + public static final long TRANSIENT_TASKBAR_STASH_ALPHA_DURATION = 50; /** * How long to delay the icon/stash handle alpha for the home to app taskbar animation. @@ -177,7 +198,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba // Duration for which an unlock event is considered "current", as other events are received // asynchronously. - private static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200; + public static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200; /** * The default stash animation, morphing the taskbar into the navbar. @@ -201,6 +222,13 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba */ private static final int TRANSITION_UNSTASH_SUW_MANUAL = 3; + /** + * total duration of entering dream state animation, which we use as start delay to + * applyState() when SYSUI_STATE_DEVICE_DREAMING flag is present. Keep this in sync with + * DreamAnimationController.TOTAL_ANIM_DURATION. + */ + private static final int SKIP_TOTAL_DREAM_ANIM_DURATION = 450; + @Retention(RetentionPolicy.SOURCE) @IntDef(value = { TRANSITION_DEFAULT, @@ -237,15 +265,17 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private @Nullable AnimatorSet mAnimator; private boolean mIsSystemGestureInProgress; - private boolean mIsImeShowing; - private boolean mIsImeSwitcherShowing; + /** Whether the IME is visible. */ + private boolean mIsImeVisible; private final Alarm mTimeoutAlarm = new Alarm(); private boolean mEnableBlockingTimeoutDuringTests = false; private Animator mTaskbarBackgroundAlphaAnimator; - private long mTaskbarBackgroundDuration; - private boolean mIsGoingHome; + private final long mTaskbarBackgroundDuration; + private boolean mUserIsNotGoingHome = false; + + private final boolean mInAppStateAffectsDesktopTasksVisibilityInTaskbar; // Evaluate whether the handle should be stashed private final LongPredicate mIsStashedPredicate = flags -> { @@ -266,36 +296,41 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private boolean mIsTaskbarSystemActionRegistered = false; private TaskbarSharedState mTaskbarSharedState; + // Used to mark whether we are in test mode to mark whether the nav bar shows in SUW + @VisibleForTesting + Boolean mNavbarHiddenOverrideForTest = null; + public TaskbarStashController(TaskbarActivityContext activity) { mActivity = activity; mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity); mAccessibilityManager = mActivity.getSystemService(AccessibilityManager.class); - mTaskbarBackgroundDuration = - activity.getResources().getInteger(R.integer.taskbar_background_duration); + // Taskbar, via `TaskbarDesktopModeController`, depends on `TaskbarStashController` state to + // determine whether desktop tasks should be shown because taskbar is pinned on the home + // screen for freeform windowing displays. In this case, list of items shown in the taskbar + // needs to be updated when in-app state changes. + // TODO(b/390665752): Feature to "lock" pinned taskbar to home screen will be superseded by + // pinning, in other launcher states, at which point this variable can be removed. + mInAppStateAffectsDesktopTasksVisibilityInTaskbar = + !mActivity.showDesktopTaskbarForFreeformDisplay() + && mActivity.showLockedTaskbarOnHome(); + + mTaskbarBackgroundDuration = activity.getResources().getInteger( + R.integer.taskbar_background_duration); if (mActivity.isPhoneMode()) { mUnstashedHeight = mActivity.getResources().getDimensionPixelSize( R.dimen.taskbar_phone_size); mStashedHeight = mActivity.getResources().getDimensionPixelSize( R.dimen.taskbar_stashed_size); } else { - mUnstashedHeight = mActivity.getDeviceProfile().taskbarHeight; - mStashedHeight = mActivity.getDeviceProfile().stashedTaskbarHeight; + mUnstashedHeight = mActivity.getDeviceProfile().getTaskbarProfile().getHeight(); + mStashedHeight = mActivity + .getDeviceProfile() + .getTaskbarProfile() + .getStashedTaskbarHeight(); } } - /** - * Show Taskbar upon receiving broadcast - */ - public void showTaskbarFromBroadcast() { - // If user is in middle of taskbar education handle go to next step of education - if (mControllers.taskbarEduTooltipController.isBeforeTooltipFeaturesStep()) { - mControllers.taskbarEduTooltipController.hide(); - mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); - } - updateAndAnimateTransientTaskbar(false); - } - /** * Initializes the controller */ @@ -323,7 +358,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba StashedHandleViewController.ALPHA_INDEX_STASHED); mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale(); - boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity); + boolean isTransientTaskbar = mActivity.isTransientTaskbar(); boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible; boolean isStashedInAppAuto = isTransientTaskbar && !mTaskbarSharedState.getTaskbarWasPinned(); @@ -338,12 +373,25 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba // For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell // us that we're paused until a bit later. This avoids flickering upon recreating taskbar. updateStateForFlag(FLAG_IN_APP, true); + updateStateForFlag(FLAG_IN_OVERVIEW, mTaskbarSharedState.isTaskbarOnOverview); + updateStateForFlag(FLAG_STASHED_BUBBLE_BAR_ON_PHONE, mActivity.isBubbleBarOnPhone()); + applyState(/* duration = */ 0); - if (mTaskbarSharedState.getTaskbarWasPinned() - || !mTaskbarSharedState.taskbarWasStashedAuto) { - tryStartTaskbarTimeout(); - } + + // Hide the background while stashed so it doesn't show on fast swipes home + boolean shouldHideTaskbarBackground = mActivity.isPhoneMode() || + (enableScalingRevealHomeAnimation() && isTransientTaskbar && isStashed()); + + mTaskbarBackgroundAlphaForStash.setValue(shouldHideTaskbarBackground ? 0 : 1); + notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp()); + + mControllers.runAfterInit(() -> { + // if taskbar should auto stash attempt to start timeout. + if (shouldAllowTaskbarToAutoStash()) { + tryStartTaskbarTimeout(); + } + }); } /** @@ -377,8 +425,10 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * Returns how long the stash/unstash animation should play. */ public long getStashDuration() { - return DisplayController.isTransientTaskbar(mActivity) - ? TRANSIENT_TASKBAR_STASH_DURATION + if (mActivity.isPinnedTaskbar()) { + return PINNED_TASKBAR_TRANSITION_DURATION; + } + return mActivity.isTransientTaskbar() ? TRANSIENT_TASKBAR_STASH_DURATION : TASKBAR_STASH_DURATION; } @@ -389,6 +439,28 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return mIsStashed; } + public boolean isDeviceLocked() { + return hasAnyFlag(FLAG_STASHED_DEVICE_LOCKED); + } + + /** + * Sets the hotseat stashed. + * b/373429249 - we might change this behavior if we remove the scrim, that's why we're keeping + * this method + */ + public void stashHotseat(boolean stash) { + mControllers.uiController.stashHotseat(stash); + } + + /** + * Instantly un-stashes the hotseat. + * * b/373429249 - we might change this behavior if we remove the scrim, that's why we're + * keeping this method + */ + public void unStashHotseatInstantly() { + mControllers.uiController.unStashHotseatInstantly(); + } + /** * Returns whether the taskbar should be stashed in apps (e.g. user long pressed to stash). */ @@ -423,13 +495,32 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return hasAnyFlag(FLAGS_IN_APP); } + /** Returns whether the taskbar is currently in overview screen. */ + public boolean isInOverview() { + return hasAnyFlag(FLAG_IN_OVERVIEW); + } + + /** Returns whether the taskbar is currently on launcher home screen. */ + public boolean isOnHome() { + return !isInOverview() && !isInApp(); + } + + /** Returns whether taskbar is hidden for bubbles. */ + public boolean isHiddenForBubbles() { + return hasAnyFlag(FLAG_STASHED_FOR_BUBBLES); + } + /** * Returns the height that taskbar will be touchable. */ public int getTouchableHeight() { return mIsStashed ? mStashedHeight - : (mUnstashedHeight + mActivity.getDeviceProfile().taskbarBottomMargin); + : (mUnstashedHeight + + mActivity.getDeviceProfile() + .getTaskbarProfile() + .getBottomMargin() + ); } /** @@ -439,18 +530,21 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * @see android.view.WindowInsets.Type#systemBars() */ public int getContentHeightToReportToApps() { - if (mActivity.isUserSetupComplete() && (mActivity.isPhoneGestureNavMode() - || DisplayController.isTransientTaskbar(mActivity))) { + boolean isTransient = mActivity.isTransientTaskbar(); + if (mActivity.isUserSetupComplete() && (mActivity.isPhoneGestureNavMode() || isTransient)) { return getStashedHeight(); } if (supportsVisualStashing() && hasAnyFlag(FLAGS_REPORT_STASHED_INSETS_TO_APP)) { DeviceProfile dp = mActivity.getDeviceProfile(); - if (hasAnyFlag(FLAG_STASHED_IN_APP_SETUP) && (dp.isTaskbarPresent - || mActivity.isPhoneGestureNavMode())) { - // We always show the back button in SUW but in portrait the SUW layout may not - // be wide enough to support overlapping the nav bar with its content. - // We're sending different res values in portrait vs landscape + // If the navigation bar is hidden in SUW, we can draw the SUW content lower so we avoid + // reporting a higher inset + if (hasAnyFlag(FLAG_STASHED_IN_APP_SETUP) + && (dp.isTaskbarPresent || mActivity.isPhoneGestureNavMode()) + && !isNavbarHiddeninSUW()) { + // When we show the back button in SUW, the SUW layout may not be wide enough to + // support overlapping the nav bar with its content in portrait. So we send + // different res values in portrait vs landscape return mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_suw_insets); } boolean isAnimating = mAnimator != null && mAnimator.isStarted(); @@ -468,6 +562,17 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return mUnstashedHeight; } + /** + * Returns whether the navigation bar is visible during the Setup Wizard. + * + * {@link #mNavbarHiddenOverrideForTest} is only used by tests + */ + private boolean isNavbarHiddeninSUW() { + // Check if a test override is active + return Objects.requireNonNullElseGet(mNavbarHiddenOverrideForTest, + () -> mControllers.navbarButtonsViewController.isNavbarHiddenInSUW()); + } + /** * Returns the height that taskbar will inset when inside apps. * @@ -485,9 +590,17 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION. * If bubble bar exists, it will match taskbars stashing behavior. + * Will not delay taskbar background by default. */ public void updateAndAnimateTransientTaskbar(boolean stash) { - updateAndAnimateTransientTaskbar(stash, /* shouldBubblesFollow= */ true); + updateAndAnimateTransientTaskbar(stash, SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE, false); + } + + /** + * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION. + */ + public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) { + updateAndAnimateTransientTaskbar(stash, shouldBubblesFollow, false); } /** @@ -495,28 +608,47 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * * @param stash whether transient taskbar should be stashed. * @param shouldBubblesFollow whether bubbles should match taskbars behavior. + * @param delayTaskbarBackground whether we will delay the taskbar background animation */ - public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) { - if (!DisplayController.isTransientTaskbar(mActivity)) { + public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow, + boolean delayTaskbarBackground) { + if (!mActivity.isTransientTaskbar() || mActivity.isBubbleBarOnPhone()) { return; } - if ( - stash - && !mControllers.taskbarAutohideSuspendController - .isSuspendedForTransientTaskbarInLauncher() - && mControllers.taskbarAutohideSuspendController - .isTransientTaskbarStashingSuspended()) { + if (stash + && !mControllers.taskbarAutohideSuspendController + .isSuspendedForTransientTaskbarInLauncher() + && mControllers.taskbarAutohideSuspendController + .isTransientTaskbarStashingSuspended()) { // Avoid stashing if autohide is currently suspended. return; } + boolean shouldApplyState = false; + + if (delayTaskbarBackground) { + mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG, true); + shouldApplyState = true; + } + if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) { mTaskbarSharedState.taskbarWasStashedAuto = stash; updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash); + shouldApplyState = true; + } + + if (shouldApplyState) { applyState(); } + // Effectively a no-opp to remove the tag. + if (delayTaskbarBackground) { + mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG, + false); + mControllers.taskbarStashController.applyState(0); + } + mControllers.bubbleControllers.ifPresent(controllers -> { if (shouldBubblesFollow) { final boolean willStash = mIsStashedPredicate.test(mState); @@ -546,10 +678,50 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /* shouldBubblesFollow= */ !bubbleBarExpanded); } + /** + * @return if we should allow taskbar to auto stash + */ + public boolean shouldAllowTaskbarToAutoStash() { + if (mActivity.isTransientTaskbar()) { + return true; + } + + boolean isTaskbarPinningOnInDesktopMode = LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE.get( + mActivity); + return !isTaskbarPinningOnInDesktopMode && mActivity.isTaskbarShowingDesktopTasks(); + } + + /** + * Stashes pinned taskbar after it has timed out. + */ + public void updateAndAnimatePinnedTaskbarForTimeout() { + updateAndAnimatePinnedTaskbar(true); + } + + /** + * Handles stashing/un-stashing taskbar in desktop mode. + */ + public void updateAndAnimatePinnedTaskbar(boolean isStashed) { + boolean shouldApplyState = false; + if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != isStashed) { + updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, isStashed); + shouldApplyState = true; + } + if (shouldApplyState) { + applyState(); + } + } + /** Toggles the Taskbar's stash state. */ public void toggleTaskbarStash() { - if (!DisplayController.isTransientTaskbar(mActivity) || !hasAnyFlag(FLAGS_IN_APP)) return; - updateAndAnimateTransientTaskbar(!hasAnyFlag(FLAG_STASHED_IN_APP_AUTO)); + if (!shouldAllowTaskbarToAutoStash() || !hasAnyFlag(FLAGS_IN_APP)) { + return; + } + if (mActivity.isTransientTaskbar()) { + updateAndAnimateTransientTaskbar(!hasAnyFlag(FLAG_STASHED_IN_APP_AUTO)); + } else if (mActivity.isTaskbarShowingDesktopTasks()) { + updateAndAnimatePinnedTaskbar(!hasAnyFlag(FLAG_STASHED_IN_APP_AUTO)); + } } /** @@ -570,6 +742,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /* isStashed= */ mActivity.isPhoneMode(), placeholderDuration, TRANSITION_UNSTASH_SUW_MANUAL, + /* skipTaskbarBackgroundDelay */ false, /* jankTag= */ "SUW_MANUAL"); animation.addListener(AnimatorListeners.forEndCallback( () -> mControllers.taskbarViewController.setDeferUpdatesForSUW(false))); @@ -579,13 +752,14 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * Create a stash animation and save to {@link #mAnimator}. * - * @param isStashed whether it's a stash animation or an unstash animation - * @param duration duration of the animation - * @param animationType what transition type to play. - * @param jankTag tag to be used in jank monitor trace. + * @param isStashed whether it's a stash animation or an unstash animation + * @param duration duration of the animation + * @param animationType what transition type to play. + * @param shouldDelayBackground whether we should delay the taskbar bg animation + * @param jankTag tag to be used in jank monitor trace. */ private void createAnimToIsStashed(boolean isStashed, long duration, - @StashAnimation int animationType, String jankTag) { + @StashAnimation int animationType, boolean shouldDelayBackground, String jankTag) { if (animationType == TRANSITION_UNSTASH_SUW_MANUAL && isStashed) { // The STASH_ANIMATION_SUW_MANUAL must only be used during an unstash animation. Log.e(TAG, "Illegal arguments:Using TRANSITION_UNSTASH_SUW_MANUAL to stash taskbar"); @@ -597,8 +771,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba mAnimator = new AnimatorSet(); addJankMonitorListener( mAnimator, /* expanding= */ !isStashed, /* tag= */ jankTag); - boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity); - final float stashTranslation = mActivity.isPhoneMode() || isTransientTaskbar + final float stashTranslation = mActivity.isPhoneMode() || mActivity.isTransientTaskbar() ? 0 : (mUnstashedHeight - mStashedHeight); @@ -622,8 +795,11 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return; } - if (isTransientTaskbar) { - createTransientAnimToIsStashed(mAnimator, isStashed, duration, animationType); + if (mActivity.isTransientTaskbar()) { + createTransientAnimToIsStashed(mAnimator, isStashed, duration, + shouldDelayBackground, animationType); + } else if (shouldAllowTaskbarToAutoStash()) { + createAnimToIsStashedPinnedTaskbar(mAnimator, isStashed, duration); } else { createAnimToIsStashed(mAnimator, isStashed, duration, stashTranslation, animationType); } @@ -653,6 +829,16 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba }); } + private void createAnimToIsStashedPinnedTaskbar(AnimatorSet as, boolean isStashed, + long duration) { + int stashTranslation = !isStashed ? 0 : mUnstashedHeight; + as.play(mIconTranslationYForStash.animateToValue(stashTranslation)); + as.play(mTaskbarBackgroundOffset.animateToValue(isStashed ? 1 : 0)); + as.play(mIconAlphaForStash.animateToValue(isStashed ? 0 : 1)); + as.play(mIconScaleForStash.animateToValue(1)); + as.setDuration(duration); + } + private void createAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration, float stashTranslation, @StashAnimation int animationType) { AnimatorSet fullLengthAnimatorSet = new AnimatorSet(); @@ -729,7 +915,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } private void createTransientAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration, - @StashAnimation int animationType) { + boolean shouldDelayBackground, @StashAnimation int animationType) { // Target values of the properties this is going to set final float backgroundOffsetTarget = isStashed ? 1 : 0; final float iconAlphaTarget = isStashed ? 0 : 1; @@ -743,14 +929,14 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba if (animationType == TRANSITION_HANDLE_FADE) { // When fading, the handle fades in/out at the beginning of the transition with // TASKBAR_STASH_ALPHA_DURATION. - backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION; + backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION; // The iconAlphaDuration must be set to duration for the skippable interpolators // below to work. iconAlphaDuration = duration; } else { iconAlphaStartDelay = TASKBAR_STASH_ALPHA_START_DELAY; - iconAlphaDuration = TASKBAR_STASH_ALPHA_DURATION; - backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION; + iconAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION; + backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION; if (isStashed) { if (animationType == TRANSITION_HOME_TO_APP) { @@ -767,7 +953,10 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba backgroundAndHandleAlphaStartDelay, backgroundAndHandleAlphaDuration, LINEAR); - if (enableScalingRevealHomeAnimation() && !isStashed) { + + if (enableScalingRevealHomeAnimation() + && !isStashed + && shouldDelayBackground) { play(as, getTaskbarBackgroundAnimatorWhenNotGoingHome(duration), 0, 0, LINEAR); as.addListener(AnimatorListeners.forEndCallback( @@ -828,17 +1017,13 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private boolean mTaskbarBgAlphaAnimationStarted = false; @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { - if (mIsGoingHome) { - mTaskbarBgAlphaAnimationStarted = true; - } if (mTaskbarBgAlphaAnimationStarted) { return; } if (valueAnimator.getAnimatedFraction() >= ANIMATED_FRACTION_THRESHOLD) { - if (!mIsGoingHome) { + if (mUserIsNotGoingHome) { playTaskbarBackgroundAlphaAnimation(); - setUserIsGoingHome(false); mTaskbarBgAlphaAnimationStarted = true; } } @@ -850,8 +1035,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * Sets whether the user is going home based on the current gesture. */ - public void setUserIsGoingHome(boolean isGoingHome) { - mIsGoingHome = isGoingHome; + public void setUserIsNotGoingHome(boolean userIsNotGoingHome) { + mUserIsNotGoingHome = userIsNotGoingHome; } /** @@ -896,7 +1081,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND : InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE; - animator.addListener(new AnimatorListenerAdapter() { + animator.addListener(new AnimationSuccessListener() { @Override public void onAnimationStart(@NonNull Animator animation) { final Configuration.Builder builder = @@ -908,9 +1093,16 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } @Override - public void onAnimationEnd(@NonNull Animator animation) { + public void onAnimationSuccess(@NonNull Animator animator) { InteractionJankMonitor.getInstance().end(action); } + + @Override + public void onAnimationCancel(@NonNull Animator animation) { + super.onAnimationCancel(animation); + + InteractionJankMonitor.getInstance().cancel(action); + } }); } @@ -940,13 +1132,29 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } public void applyState() { - applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION); + applyState(/* postApplyAction = */ null); + } + + /** Applies state and performs action after state is applied. */ + public void applyState(@Nullable Runnable postApplyAction) { + applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION, postApplyAction); } public void applyState(long duration) { + applyState(duration, /* postApplyAction = */ null); + } + + private void applyState(long duration, @Nullable Runnable postApplyAction) { Animator animator = createApplyStateAnimator(duration); if (animator != null) { + if (postApplyAction != null) { + // performs action on animation end + animator.addListener(AnimatorListeners.forEndCallback(postApplyAction)); + } animator.start(); + } else if (postApplyAction != null) { + // animator was not created, just execute the action + postApplyAction.run(); } } @@ -964,6 +1172,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba */ @Nullable public Animator createApplyStateAnimator(long duration) { + if (mActivity.isPhoneMode()) { + return null; + } return mStatePropertyHolder.createSetStateAnimator(mState, duration); } @@ -996,8 +1207,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * When hiding the IME, delay the unstash animation to align with the end of the transition. */ - private long getTaskbarStashStartDelayForIme() { - if (mIsImeShowing) { + @VisibleForTesting + long getTaskbarStashStartDelayForIme() { + if (mIsImeVisible) { // Only delay when IME is exiting, not entering. return 0; } @@ -1013,28 +1225,37 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba long startDelay = 0; updateStateForFlag(FLAG_STASHED_IN_APP_SYSUI, hasAnyFlag(systemUiStateFlags, - SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE)); + SYSUI_STATE_DIALOG_SHOWING | (ENABLE_TASKBAR_BEHIND_SHADE.isTrue() + ? 0 + : SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) + )); boolean stashForBubbles = hasAnyFlag(FLAG_IN_OVERVIEW) && hasAnyFlag(systemUiStateFlags, SYSUI_STATE_BUBBLES_EXPANDED) - && DisplayController.isTransientTaskbar(mActivity); + && mActivity.isTransientTaskbar(); updateStateForFlag(FLAG_STASHED_SYSUI, hasAnyFlag(systemUiStateFlags, SYSUI_STATE_SCREEN_PINNING) || stashForBubbles); + updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, SystemUiFlagUtils.isLocked(systemUiStateFlags)); - mIsImeShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SHOWING); - mIsImeSwitcherShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SWITCHER_SHOWING); + mIsImeVisible = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_VISIBLE); if (updateStateForFlag(FLAG_STASHED_IME, shouldStashForIme())) { animDuration = TASKBAR_STASH_DURATION_FOR_IME; startDelay = getTaskbarStashStartDelayForIme(); } - applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay); + if (isTaskbarHidden(systemUiStateFlags) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) { + updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags)); + applyState(0, SKIP_TOTAL_DREAM_ANIM_DURATION); + } else { + updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags)); + applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay); + } } /** - * We stash when IME or IME switcher is showing. + * We stash when the IME is visible. * *

Do not stash if in small screen, with 3 button nav, and in landscape (or seascape). *

Do not stash if taskbar is transient. @@ -1042,26 +1263,26 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba *

Do not stash if a system gesture is started. */ private boolean shouldStashForIme() { - if (DisplayController.isTransientTaskbar(mActivity)) { + if (mActivity.isTransientTaskbar()) { return false; } // Do not stash if in small screen, with 3 button nav, and in landscape. if (mActivity.isPhoneMode() && mActivity.isThreeButtonNav() - && mActivity.getDeviceProfile().isLandscape) { + && mActivity.getDeviceProfile().getDeviceProperties().isLandscape()) { return false; } // Do not stash if pinned taskbar, hardware keyboard is attached and no IME is docked - if (mActivity.isHardwareKeyboard() && DisplayController.isPinnedTaskbar(mActivity) + if (mActivity.isHardwareKeyboard() && mActivity.isPinnedTaskbar() && !mActivity.isImeDocked()) { return false; } // Do not stash if hardware keyboard is attached, in 3 button nav and desktop windowing mode - DesktopVisibilityController visibilityController = - LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); - if (visibilityController != null && mActivity.isHardwareKeyboard() - && mActivity.isThreeButtonNav() && visibilityController.areDesktopTasksVisible()) { + if (mActivity.isHardwareKeyboard() + && mActivity.isThreeButtonNav() + && mControllers.taskbarDesktopModeController + .isInDesktopModeAndNotInOverview(mActivity.getDisplayId())) { return false; } @@ -1070,7 +1291,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return false; } - return mIsImeShowing || mIsImeSwitcherShowing; + return mIsImeVisible; } /** @@ -1116,6 +1337,14 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR, !hasAnyFlag(FLAG_STASHED_IN_APP_AUTO)); } + if (hasAnyFlag(changedFlags, FLAG_IN_OVERVIEW | FLAG_IN_APP)) { + mControllers.runAfterInit(() -> mControllers.taskbarInsetsController + .onTaskbarOrBubblebarWindowHeightOrInsetsChanged()); + if (mInAppStateAffectsDesktopTasksVisibilityInTaskbar) { + mControllers.runAfterInit( + () -> mControllers.taskbarViewController.commitRunningAppsToUI()); + } + } mActivity.applyForciblyShownFlagWhileTransientTaskbarUnstashed(!isStashedInApp()); } @@ -1130,7 +1359,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba */ public void setUpTaskbarSystemAction(boolean visible) { UI_HELPER_EXECUTOR.execute(() -> { - if (!visible || !DisplayController.isTransientTaskbar(mActivity)) { + if (!visible || !mActivity.isTransientTaskbar() + || mActivity.isPhoneMode()) { mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR); mIsTaskbarSystemActionRegistered = false; return; @@ -1154,6 +1384,13 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * Clean up on destroy from TaskbarControllers */ public void onDestroy() { + // If the controller is destroyed before the animation finishes, we cancel the animation + // so that we don't finish the CUJ. + if (mAnimator != null) { + mAnimator.cancel(); + mAnimator = null; + } + mTaskbarSharedState.isTaskbarOnOverview = hasAnyFlag(FLAG_IN_OVERVIEW); UI_HELPER_EXECUTOR.execute( () -> mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR)); } @@ -1174,7 +1411,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * If false, attempts to re/start the timeout */ public void updateTaskbarTimeout(boolean isAutohideSuspended) { - if (!DisplayController.isTransientTaskbar(mActivity)) { + if (!shouldAllowTaskbarToAutoStash()) { return; } if (isAutohideSuspended) { @@ -1187,10 +1424,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba /** * Attempts to start timer to auto hide the taskbar based on time. */ - public void tryStartTaskbarTimeout() { - if (!DisplayController.isTransientTaskbar(mActivity) - || mIsStashed - || mEnableBlockingTimeoutDuringTests) { + private void tryStartTaskbarTimeout() { + if (!shouldAllowTaskbarToAutoStash() || mIsStashed || mEnableBlockingTimeoutDuringTests) { return; } @@ -1212,7 +1447,16 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba if (mControllers.taskbarAutohideSuspendController.isTransientTaskbarStashingSuspended()) { return; } - updateAndAnimateTransientTaskbarForTimeout(); + if (mActivity.isTransientTaskbar()) { + updateAndAnimateTransientTaskbarForTimeout(); + } else if (shouldAllowTaskbarToAutoStash()) { + updateAndAnimatePinnedTaskbarForTimeout(); + } + } + + @VisibleForTesting + Alarm getTimeoutAlarm() { + return mTimeoutAlarm; } @Override @@ -1225,8 +1469,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba pw.println(prefix + "\tappliedState=" + getStateString(mStatePropertyHolder.mPrevFlags)); pw.println(prefix + "\tmState=" + getStateString(mState)); pw.println(prefix + "\tmIsSystemGestureInProgress=" + mIsSystemGestureInProgress); - pw.println(prefix + "\tmIsImeShowing=" + mIsImeShowing); - pw.println(prefix + "\tmIsImeSwitcherShowing=" + mIsImeSwitcherShowing); + pw.println(prefix + "\tmIsImeVisible=" + mIsImeVisible); } private static String getStateString(long flags) { @@ -1268,6 +1511,13 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba */ @Nullable public Animator createSetStateAnimator(long flags, long duration) { + // We do this when we want to synchronize the app launch and taskbar stash animations. + if (syncAppLaunchWithTaskbarStash() + && hasAnyFlag(FLAG_IGNORE_IN_APP) + && hasAnyFlag(flags, FLAG_IN_APP)) { + flags = flags & ~FLAG_IN_APP; + } + boolean isStashed = mStashCondition.test(flags); if (DEBUG) { @@ -1317,8 +1567,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba mIsStashed = isStashed; mLastStartedTransitionType = animationType; + boolean shouldDelayBackground = hasAnyFlag(FLAG_DELAY_TASKBAR_BG_TAG); // This sets mAnimator. - createAnimToIsStashed(mIsStashed, duration, animationType, + createAnimToIsStashed(mIsStashed, duration, animationType, shouldDelayBackground, computeTaskbarJankMonitorTag(changedFlags)); return mAnimator; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt index deaf0244e9..fd259223d2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt @@ -23,7 +23,6 @@ import com.android.launcher3.testing.shared.ResourceUtils import com.android.launcher3.touch.SingleAxisSwipeDetector import com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE import com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL -import com.android.launcher3.util.DisplayController import com.android.launcher3.util.TouchController import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer @@ -39,7 +38,7 @@ import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : TouchController { private val activity: TaskbarActivityContext = controllers.taskbarActivityContext - private val enabled = DisplayController.isTransientTaskbar(activity) + private val enabled = activity.isTransientTaskbar private val swipeDownDetector: SingleAxisSwipeDetector private val translationCallback = controllers.taskbarTranslationController.transitionCallback /** Interpolator to apply resistance as user swipes down to the bottom of the screen. */ @@ -48,7 +47,8 @@ class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : Touc private val maxVisualDisplacement = activity.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat() /** How far the swipe could go, if user swiped from the very top of TaskbarView. */ - private val maxTouchDisplacement = maxVisualDisplacement + activity.deviceProfile.taskbarHeight + private val maxTouchDisplacement = + maxVisualDisplacement + activity.deviceProfile.taskbarProfile.height private val touchDisplacementToStash = activity.resources.getDimensionPixelSize(R.dimen.taskbar_to_nav_threshold).toFloat() @@ -67,9 +67,10 @@ class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : Touc val gestureHeight: Int = ResourceUtils.getNavbarSize( ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, - activity.resources + activity.resources, ) - gestureHeightYThreshold = (activity.deviceProfile.heightPx - gestureHeight).toFloat() + gestureHeightYThreshold = + (activity.deviceProfile.deviceProperties.heightPx - gestureHeight).toFloat() } private fun createSwipeListener() = @@ -89,7 +90,7 @@ class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : Touc maxTouchDisplacement, 0f, maxVisualDisplacement, - displacementInterpolator + displacementInterpolator, ) ) return false @@ -127,6 +128,14 @@ class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : Touc if (ev.action == MotionEvent.ACTION_OUTSIDE) { controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) } else if (controllers.taskbarViewController.isEventOverAnyItem(screenCoordinatesEv)) { + // TODO (b/411155437) remove this once BubbleDragController implements TouchController + val bubbleBarDragInProgress = + controllers.bubbleControllers + .map { it.bubbleDragController.isDragging } + .orElse(false) + if (bubbleBarDragInProgress) { + return false + } swipeDownDetector.onTouchEvent(ev) if (swipeDownDetector.isDraggingState) { return true diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java index 5b6fbef4fd..d3d7f5e143 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java @@ -25,7 +25,6 @@ import androidx.core.content.res.ResourcesCompat; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; /** * Utility class that contains the different taskbar thresholds logic. @@ -39,11 +38,7 @@ public class TaskbarThresholdUtils { private static int getThreshold(Resources r, DeviceProfile dp, int thresholdDimen, int multiplierDimen) { - if (!FeatureFlags.ENABLE_DYNAMIC_TASKBAR_THRESHOLDS.get()) { - return r.getDimensionPixelSize(thresholdDimen); - } - - float landscapeScreenHeight = dp.isLandscape ? dp.heightPx : dp.widthPx; + float landscapeScreenHeight = dp.getDeviceProperties().isLandscape() ? dp.getDeviceProperties().getHeightPx() : dp.getDeviceProperties().getWidthPx(); float screenPart = (landscapeScreenHeight * SCREEN_UNITS); float defaultDp = dpiFromPx(screenPart, DisplayMetrics.DENSITY_DEVICE_STABLE); float thisDp = dpToPx(defaultDp); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java new file mode 100644 index 0000000000..615db012ec --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTransitions.java @@ -0,0 +1,135 @@ +/* + * 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.launcher3.taskbar; + +import android.view.View; + +import com.android.launcher3.R; +import com.android.launcher3.taskbar.navbutton.NearestTouchFrame; +import com.android.systemui.shared.statusbar.phone.BarTransitions; + +import java.io.PrintWriter; + +/** Manages task bar transitions */ +public class TaskbarTransitions extends BarTransitions implements + TaskbarControllers.LoggableTaskbarController { + + private final TaskbarActivityContext mContext; + + private boolean mWallpaperVisible; + + private boolean mLightsOut; + private boolean mAutoDim; + private View mNavButtons; + private float mDarkIntensity; + + private final NearestTouchFrame mView; + + public TaskbarTransitions(TaskbarActivityContext context, NearestTouchFrame view) { + super(view, R.drawable.nav_background); + + mContext = context; + mView = view; + } + + void init() { + mView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + mNavButtons = mView.findViewById(R.id.end_nav_buttons); + applyLightsOut(false, true); + }); + mNavButtons = mView.findViewById(R.id.end_nav_buttons); + + applyModeBackground(-1, getMode(), false /*animate*/); + applyLightsOut(false /*animate*/, true /*force*/); + if (mContext.isPhoneButtonNavMode()) { + mBarBackground.setOverrideAlpha(1); + } + } + + void setWallpaperVisibility(boolean visible) { + mWallpaperVisible = visible; + applyLightsOut(true, false); + } + + @Override + public void setAutoDim(boolean autoDim) { + // Ensure we aren't in gestural nav if we are triggering auto dim + if (autoDim && !mContext.isPhoneButtonNavMode()) { + return; + } + if (mAutoDim == autoDim) return; + mAutoDim = autoDim; + applyLightsOut(true, false); + } + + @Override + protected void onTransition(int oldMode, int newMode, boolean animate) { + super.onTransition(oldMode, newMode, animate); + applyLightsOut(animate, false /*force*/); + } + + private void applyLightsOut(boolean animate, boolean force) { + // apply to lights out + applyLightsOut(isLightsOut(getMode()), animate, force); + } + + private void applyLightsOut(boolean lightsOut, boolean animate, boolean force) { + if (!force && lightsOut == mLightsOut) return; + + mLightsOut = lightsOut; + if (mNavButtons == null) return; + + // ok, everyone, stop it right there + mNavButtons.animate().cancel(); + + // Bump percentage by 10% if dark. + float darkBump = mDarkIntensity / 10; + final float navButtonsAlpha = lightsOut ? 0.6f + darkBump : 1f; + + if (!animate) { + mNavButtons.setAlpha(navButtonsAlpha); + } else { + final int duration = lightsOut ? LIGHTS_OUT_DURATION : LIGHTS_IN_DURATION; + mNavButtons.animate() + .alpha(navButtonsAlpha) + .setDuration(duration) + .start(); + } + } + + void onDarkIntensityChanged(float darkIntensity) { + mDarkIntensity = darkIntensity; + if (mAutoDim) { + applyLightsOut(false, true); + } + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "TaskbarTransitions:"); + + pw.println(prefix + "\tmMode=" + getMode()); + pw.println(prefix + "\tmAlwaysOpaque: " + isAlwaysOpaque()); + pw.println(prefix + "\tmWallpaperVisible: " + mWallpaperVisible); + pw.println(prefix + "\tmLightsOut: " + mLightsOut); + pw.println(prefix + "\tmAutoDim: " + mAutoDim); + pw.println(prefix + "\tbg overrideAlpha: " + mBarBackground.getOverrideAlpha()); + pw.println(prefix + "\tbg color: " + mBarBackground.getColor()); + pw.println(prefix + "\tbg frame: " + mBarBackground.getFrame()); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java index 144c0c25c8..13fb296498 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java @@ -29,7 +29,6 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.app.animation.Interpolators; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.SpringAnimationBuilder; -import com.android.launcher3.util.DisplayController; import java.io.PrintWriter; @@ -63,7 +62,7 @@ public class TaskbarTranslationController implements TaskbarControllers.Loggable public TaskbarTranslationController(TaskbarActivityContext context) { mContext = context; - mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext); + mIsTransientTaskbar = mContext.isTransientTaskbar(); mCallback = new TransitionCallback(); } @@ -95,7 +94,8 @@ public class TaskbarTranslationController implements TaskbarControllers.Loggable mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY); mControllers.bubbleControllers.ifPresent(controllers -> { controllers.bubbleBarViewController.setTranslationYForSwipe(transY); - controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY); + controllers.bubbleStashedHandleViewController.ifPresent( + controller -> controller.setTranslationYForSwipe(transY)); }); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index aa712f9a04..06057038f0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -20,8 +20,8 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; -import static com.android.quickstep.OverviewCommandHelper.TYPE_HIDE; +import android.animation.Animator; import android.content.Intent; import android.graphics.drawable.BitmapDrawable; import android.view.MotionEvent; @@ -37,16 +37,17 @@ import com.android.launcher3.Utilities; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.popup.SystemShortcut; -import com.android.launcher3.util.DisplayController; +import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.util.SplitConfigurationOptions; -import com.android.quickstep.OverviewCommandHelper; -import com.android.quickstep.util.GroupTask; -import com.android.quickstep.util.TISBindHelper; +import com.android.quickstep.GestureState; +import com.android.quickstep.RecentsAnimationCallbacks; +import com.android.quickstep.util.SplitTask; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; -import com.android.quickstep.views.TaskView.TaskContainer; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.Collections; @@ -55,12 +56,14 @@ import java.util.stream.Stream; /** * Base class for providing different taskbar UI */ -public class TaskbarUIController { +public class TaskbarUIController implements BubbleBarController.BubbleBarLocationListener { public static final TaskbarUIController DEFAULT = new TaskbarUIController(); // Initialized in init. protected TaskbarControllers mControllers; + protected boolean mSkipLauncherVisibilityChange; + @CallSuper protected void init(TaskbarControllers taskbarControllers) { mControllers = taskbarControllers; @@ -69,6 +72,11 @@ public class TaskbarUIController { @CallSuper protected void onDestroy() { mControllers = null; + RecentsView recentsView = getRecentsView(); + if (recentsView != null) { + recentsView.setTaskLaunchListener(null); + recentsView.setTaskLaunchCancelledRunnable(null); + } } protected boolean isTaskbarTouchable() { @@ -76,41 +84,27 @@ public class TaskbarUIController { } /** - * This should only be called by TaskbarStashController so that a - * TaskbarUIController can + * This should only be called by TaskbarStashController so that a TaskbarUIController can * disable stashing. All other controllers should use - * {@link TaskbarStashController#supportsVisualStashing()} as the source of - * truth. + * {@link TaskbarStashController#supportsVisualStashing()} as the source of truth. */ public boolean supportsVisualStashing() { return true; } - protected void onStashedInAppChanged() { - } + protected void onStashedInAppChanged() { } /** * Called when taskbar icon layout bounds change. */ - protected void onIconLayoutBoundsChanged() { - } + protected void onIconLayoutBoundsChanged() { } protected String getTaskbarUIControllerName() { return "TaskbarUIController"; } /** Called when an icon is launched. */ - @CallSuper - public void onTaskbarIconLaunched(ItemInfo item) { - // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately - // instead of - // waiting for onPause, to reduce potential visual noise during the app open - // transition. - if (mControllers.taskbarStashController == null) - return; - mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true); - mControllers.taskbarStashController.applyState(); - } + public void onTaskbarIconLaunched(ItemInfo item) { } public View getRootView() { return mControllers.taskbarActivityContext.getDragLayer(); @@ -118,9 +112,7 @@ public class TaskbarUIController { /** * Called when swiping from the bottom nav region in fully gestural mode. - * - * @param inProgress True if the animation started, false if we just settled on - * an end target. + * @param inProgress True if the animation started, false if we just settled on an end target. */ public void setSystemGestureInProgress(boolean inProgress) { mControllers.taskbarStashController.setSystemGestureInProgress(inProgress); @@ -130,15 +122,15 @@ public class TaskbarUIController { * Manually closes the overlay window. */ public void hideOverlayWindow() { - if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext) - || mControllers.taskbarAllAppsController.isOpen()) { + mControllers.keyboardQuickSwitchController.closeQuickSwitchView(); + boolean isTransientTaskbar = mControllers.taskbarActivityContext.isTransientTaskbar(); + if (!isTransientTaskbar || mControllers.taskbarAllAppsController.isOpen()) { mControllers.taskbarOverlayController.hideWindow(); } } /** - * User expands PiP to full-screen (or split-screen) mode, try to hide the - * Taskbar. + * User expands PiP to full-screen (or split-screen) mode, try to hide the Taskbar. */ public void onExpandPip() { if (mControllers != null) { @@ -175,23 +167,27 @@ public class TaskbarUIController { mControllers.taskbarActivityContext.startTranslationSpring(); } + /** + * @return if we should allow taskbar to auto stash + */ + public boolean shouldAllowTaskbarToAutoStash() { + return mControllers.taskbarActivityContext.shouldAllowTaskbarToAutoStash(); + } + /** * @param ev MotionEvent in screen coordinates. - * - * @return Whether any Taskbar item could handle the given MotionEvent if given - * the chance. + * @return Whether any Taskbar item could handle the given MotionEvent if given the chance. */ public boolean isEventOverAnyTaskbarItem(MotionEvent ev) { return mControllers.taskbarViewController.isEventOverAnyItem(ev) || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev); } - /** - * Checks if the given {@link MotionEvent} is over the bubble bar stash handle. - */ - public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) { + /** Checks if the given {@link MotionEvent} is over the bubble bar views. */ + public boolean isEventOverBubbleBarViews(MotionEvent ev) { return mControllers.bubbleControllers.map( - bubbleControllers -> bubbleControllers.bubbleStashController.isEventOverStashHandle(ev)) + bubbleControllers -> + bubbleControllers.bubbleStashController.isEventOverBubbleBarViews(ev)) .orElse(false); } @@ -203,23 +199,48 @@ public class TaskbarUIController { } /** - * Returns true if hotseat icons are on top of view hierarchy when aligned in - * the current state. + * Returns true if hotseat icons are on top of view hierarchy when aligned in the current state. */ public boolean isHotseatIconOnTopWhenAligned() { return true; } + public boolean isAnimatingToHotseat() { + return false; + } + + /** + * Skips to the end of the animation to Hotseat - should only be used if + * {@link #isAnimatingToHotseat()} returns true. + */ + public void endAnimationToHotseat() {} + /** Returns {@code true} if Taskbar is currently within overview. */ protected boolean isInOverviewUi() { return false; } + /** - * Returns {@code true} if Home All Apps available instead of Taskbar All Apps. + * Toggles all apps UI. Default implementation opens Taskbar All Apps, but may be overridden to + * open different Alls Apps variant depending on the context. + * @param focusSearch indicates whether All Apps should be opened with search input focused. */ - protected boolean canToggleHomeAllApps() { - return false; + protected void toggleAllApps(boolean focusSearch) { + if (focusSearch) { + mControllers.taskbarAllAppsController.toggleSearch(); + } else { + mControllers.taskbarAllAppsController.toggle(); + } + } + + public boolean isDraggingItem() { + boolean bubblesDragging = false; + if (mControllers.bubbleControllers.isPresent()) { + bubblesDragging = + mControllers.bubbleControllers.get().bubbleDragController.isDragging(); + } + return mControllers.taskbarDragController.isDragging() || bubblesDragging; } @CallSuper @@ -232,10 +253,8 @@ public class TaskbarUIController { /** * Returns RecentsView. Overwritten in LauncherTaskbarUIController and - * FallbackTaskbarUIController with Launcher-specific implementations. Returns - * null for other - * UI controllers (like DesktopTaskbarUIController) that don't have a - * RecentsView. + * FallbackTaskbarUIController with Launcher-specific implementations. Returns null for other + * UI controllers (like DesktopTaskbarUIController) that don't have a RecentsView. */ public @Nullable RecentsView getRecentsView() { return null; @@ -248,30 +267,36 @@ public class TaskbarUIController { } recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback( - Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()), + Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()), false /* findExactPairMatch */, foundTasks -> { - @Nullable - Task foundTask = foundTasks[0]; + @Nullable Task foundTask = foundTasks[0]; splitSelectSource.alreadyRunningTaskId = foundTask == null ? INVALID_TASK_ID : foundTask.key.id; splitSelectSource.animateCurrentTaskDismissal = foundTask != null; recentsView.initiateSplitSelect(splitSelectSource); - }); + } + ); } /** * Uses the clicked Taskbar icon to launch a second app for splitscreen. */ public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) { + // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately + // to reduce potential visual noise during the app open transition. + if (mControllers.taskbarStashController != null) { + mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true); + mControllers.taskbarStashController.applyState(); + } + RecentsView recents = getRecentsView(); recents.getSplitSelectController().findLastActiveTasksAndRunCallback( Collections.singletonList(info.getComponentKey()), false /* findExactPairMatch */, foundTasks -> { - @Nullable - Task foundTask = foundTasks[0]; + @Nullable Task foundTask = foundTasks[0]; if (foundTask != null) { TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id); // TODO (b/266482558): This additional null check is needed because there @@ -282,13 +307,14 @@ public class TaskbarUIController { if (foundTaskView != null) { // There is already a running app of this type, use that as second app. // Get index of task (0 or 1), in case it's a GroupedTaskView - TaskContainer taskContainer = foundTaskView.getTaskContainerById(foundTask.key.id); + TaskContainer taskContainer = + foundTaskView.getTaskContainerById(foundTask.key.id); recents.confirmSplitSelect( foundTaskView, foundTask, taskContainer.getIconView().getDrawable(), - taskContainer.getThumbnailViewDeprecated(), - taskContainer.getThumbnailViewDeprecated().getThumbnail(), + taskContainer.getSnapshotView(), + taskContainer.getThumbnail(), null /* intent */, null /* user */, info); @@ -306,14 +332,14 @@ public class TaskbarUIController { intent, info.user, info); - }); + } + ); } /** * Opens the Keyboard Quick Switch View. * - * This will set the focus to the first task from the right (from the left in - * RTL) + * This will set the focus to the first task from the right (from the left in RTL) */ public void openQuickSwitchView() { mControllers.keyboardQuickSwitchController.openQuickSwitchView(); @@ -322,13 +348,10 @@ public class TaskbarUIController { /** * Launches the focused task and closes the Keyboard Quick Switch View. * - * If the overlay or view are closed, or the overview task is focused, then - * Overview is - * launched. If the overview task is launched, then the first hidden task is - * focused. + * If the overlay or view are closed, or the overview task is focused, then Overview is + * launched. If the overview task is launched, then the first hidden task is focused. * - * @return the index of what task should be focused in ; -1 iff Overview - * shouldn't be launched + * @return the index of what task should be focused in ; -1 iff Overview shouldn't be launched */ public int launchFocusedTask() { int focusedTaskIndex = mControllers.keyboardQuickSwitchController.launchFocusedTask(); @@ -340,12 +363,10 @@ public class TaskbarUIController { * Launches the given task in split-screen. */ public void launchSplitTasks( - @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { - } + @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) { } /** * Returns the matching view (if any) in the taskbar. - * * @param view The view to match. */ public @Nullable View findMatchingView(View view) { @@ -357,8 +378,7 @@ public class TaskbarUIController { return null; } - // Taskbar has the same items as the hotseat and we can use screenId to find the - // match. + // Taskbar has the same items as the hotseat and we can use screenId to find the match. int screenId = info.screenId; View[] views = mControllers.taskbarViewController.getIconViews(); for (int i = views.length - 1; i >= 0; --i) { @@ -372,9 +392,7 @@ public class TaskbarUIController { } /** - * Callback for when launcher state transition completes after user swipes to - * home. - * + * Callback for when launcher state transition completes after user swipes to home. * @param finalState The final state of the transition. */ public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) { @@ -384,8 +402,7 @@ public class TaskbarUIController { /** * Refreshes the resumed state of this ui controller. */ - public void refreshResumedState() { - } + public void refreshResumedState() {} /** * Returns a stream of split screen menu options appropriate to the device. @@ -398,35 +415,19 @@ public class TaskbarUIController { } /** Adjusts the hotseat for the bubble bar. */ - public void adjustHotseatForBubbleBar(boolean isBubbleBarVisible) { - } - - @Nullable - protected TISBindHelper getTISBindHelper() { - return null; - } + public void adjustHotseatForBubbleBar(boolean isBubbleBarVisible) {} /** - * Launches the focused task in the Keyboard Quick Switch view through the - * OverviewCommandHelper + * Launches the focused task in the Keyboard Quick Switch view through the OverviewCommandHelper *

* Use this helper method when the focused task may be the overview task. */ public void launchKeyboardFocusedTask() { - TISBindHelper tisBindHelper = getTISBindHelper(); - if (tisBindHelper == null) { - return; - } - OverviewCommandHelper overviewCommandHelper = tisBindHelper.getOverviewCommandHelper(); - if (overviewCommandHelper == null) { - return; - } - overviewCommandHelper.addCommand(TYPE_HIDE); + mControllers.navButtonController.hideOverview(); } /** * Adjusts the taskbar based on the visibility of the launcher. - * * @param isVisible True if launcher is visible, false otherwise. */ public void onLauncherVisibilityChanged(boolean isVisible) { @@ -435,8 +436,7 @@ public class TaskbarUIController { } /** - * Request for UI controller to ignore animations for the next callback for the - * end of recents + * Request for UI controller to ignore animations for the next callback for the end of recents * animation */ public void setSkipNextRecentsAnimEnd() { @@ -446,7 +446,51 @@ public class TaskbarUIController { /** * Sets whether the user is going home based on the current gesture. */ - public void setUserIsGoingHome(boolean isGoingHome) { - mControllers.taskbarStashController.setUserIsGoingHome(isGoingHome); + public void setUserIsNotGoingHome(boolean isNotGoingHome) { + mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome); + } + + /** + * Sets whether to prevent taskbar from reacting to launcher visibility during the recents + * transition animation. + */ + public void setSkipLauncherVisibilityChange(boolean skip) { + mSkipLauncherVisibilityChange = skip; + } + + /** Sets whether the hotseat is stashed */ + public void stashHotseat(boolean stash) { + } + + @Override + public void onBubbleBarLocationAnimated(BubbleBarLocation location) { + } + + @Override + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + } + + /** Un-stash the hotseat instantly */ + public void unStashHotseatInstantly() { + } + + /** + * Called when we want to unstash taskbar when user performs swipes up gesture. + */ + public void onSwipeToUnstashTaskbar() { + } + + /** + * Called at the end of a gesture (see {@link GestureState.GestureEndTarget}). + * @param endTarget Where the gesture animation is going to. + * @param callbacks callbacks to track the recents animation lifecycle. The state change is + * automatically reset once the recents animation finishes + * @return An optional Animator to play in parallel with the default gesture end animation. + */ + public @Nullable Animator getParallelAnimationToGestureEndTarget( + GestureState.GestureEndTarget endTarget, + long duration, + RecentsAnimationCallbacks callbacks) { + return null; } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 5ca41df590..b55bf4a1ee 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -16,43 +16,44 @@ package com.android.launcher3.taskbar; import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_OVERFLOW; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION; import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR; import static com.android.launcher3.Flags.enableCursorHoverStates; +import static com.android.launcher3.Flags.enableRecentsInTaskbar; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER; -import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR; import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; -import android.os.Bundle; +import android.graphics.drawable.Drawable; +import android.util.ArraySet; import android.util.AttributeSet; import android.view.DisplayCutout; import android.view.InputDevice; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; -import android.view.accessibility.AccessibilityNodeInfo; +import android.view.ViewGroup; import android.widget.FrameLayout; -import androidx.annotation.DimenRes; -import androidx.annotation.DrawableRes; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.Insettable; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.apppairs.AppPairIcon; +import com.android.launcher3.celllayout.CellInfo; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.folder.PreviewBackground; import com.android.launcher3.model.data.AppPairInfo; @@ -60,24 +61,30 @@ import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.LauncherBindableItemsContainer; +import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer; +import com.android.launcher3.taskbar.customization.TaskbarDividerContainer; +import com.android.launcher3.uioverrides.PredictedAppIcon; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.views.IconButtonView; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SingleTask; +import com.android.quickstep.views.TaskViewType; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + import com.patrykmichalik.opto.core.PreferenceExtensionsKt; -import com.android.quickstep.DeviceConfigWrapper; -import com.android.quickstep.util.AssistStateManager; - -import java.util.function.Predicate; - import app.lawnchair.hotseat.HotseatMode; import app.lawnchair.preferences2.PreferenceManager2; import app.lawnchair.theme.color.tokens.ColorTokens; /** - * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of - * other apps. + * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps. */ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable, DeviceProfile.OnDeviceProfileChangeListener { @@ -92,6 +99,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private final boolean mIsRtl; private final TaskbarActivityContext mActivityContext; + @Nullable private BubbleBarLocation mBubbleBarLocation = null; // Initialized in init. private TaskbarViewCallbacks mControllerCallbacks; @@ -99,16 +107,32 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private View.OnLongClickListener mIconLongClickListener; // Only non-null when the corresponding Folder is open. - private @Nullable FolderIcon mLeaveBehindFolderIcon; + @Nullable private FolderIcon mLeaveBehindFolderIcon; // Only non-null when device supports having an All Apps button. - private @Nullable IconButtonView mAllAppsButton; - private Runnable mAllAppsTouchRunnable; - private long mAllAppsButtonTouchDelayMs; - private boolean mAllAppsTouchTriggered; + private final TaskbarAllAppsButtonContainer mAllAppsButtonContainer; - // Only non-null when device supports having an All Apps button. - private @Nullable IconButtonView mTaskbarDivider; + // Only non-null when device supports having a Divider button. + @Nullable private TaskbarDividerContainer mTaskbarDividerContainer; + + // Only non-null when device supports having a Taskbar Overflow button. + @Nullable private TaskbarOverflowView mTaskbarOverflowView; + + private int mNextViewIndex; + + public int getIgnoreTaskbarIconCount() { + return mIgnoreTaskbarIconCount; + } + + // TODO: clean it up in follow up cl with removal of taskbar icon alignment. + // Only used for edge of 3 button navigation mode, where we need to hide icons which go + // beyond the bounds. + private int mIgnoreTaskbarIconCount = 0; + /** + * Whether the divider is between Hotseat icons and Recents, + * instead of between All Apps button and Hotseat. + */ + private boolean mAddedDividerForRecents; private final View mQsb; @@ -116,6 +140,16 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private boolean mShouldTryStartAlign; + private int mMaxNumIcons = 0; + private int mIdealNumIcons = 0; + + private final int mAllAppsButtonTranslationOffset; + + private int mNumStaticViews; + + private Set mPrevRecentTasks = Collections.emptySet(); + private Set mPrevOverflowTasks = Collections.emptySet(); + public TaskbarView(@NonNull Context context) { this(context, null); } @@ -137,18 +171,17 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mActivityContext = ActivityContext.lookupContext(context); mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds(); Resources resources = getResources(); - boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivityContext) - && !mActivityContext.isPhoneMode(); mIsRtl = Utilities.isRtl(resources); mTransientTaskbarMinWidth = resources.getDimension(R.dimen.transient_taskbar_min_width); onDeviceProfileChanged(mActivityContext.getDeviceProfile()); int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing); - int actualIconSize = mActivityContext.getDeviceProfile().taskbarIconSize; - if (enableTaskbarPinning() && !mActivityContext.isThreeButtonNav()) { + int actualIconSize = + mActivityContext.getDeviceProfile().getTaskbarProfile().getIconSize(); + if (enableTaskbarPinning() && canTransitionToTransientTaskbar()) { DeviceProfile deviceProfile = mActivityContext.getTransientTaskbarDeviceProfile(); - actualIconSize = deviceProfile.taskbarIconSize; + actualIconSize = deviceProfile.getTaskbarProfile().getIconSize(); } int visualIconSize = (int) (actualIconSize * ICON_VISIBLE_AREA_FACTOR); @@ -158,34 +191,32 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // We layout the icons to be of mIconTouchSize in width and height mItemMarginLeftRight = actualMargin - (mIconTouchSize - visualIconSize) / 2; - // We always layout taskbar as a transient taskbar when we have taskbar pinning - // feature on, - // then we scale and translate the icons to match persistent taskbar designs, so - // we use - // taskbar icon size from current device profile to calculate correct item - // padding. - mItemPadding = (mIconTouchSize - mActivityContext.getDeviceProfile().taskbarIconSize) / 2; + // We always layout taskbar as a transient taskbar when we have taskbar pinning feature on, + // then we scale and translate the icons to match persistent taskbar designs, so we use + // taskbar icon size from current device profile to calculate correct item padding. + mItemPadding = (mIconTouchSize - mActivityContext + .getDeviceProfile() + .getTaskbarProfile() + .getIconSize()) / 2; mFolderLeaveBehindColor = Themes.getAttrColor(mActivityContext, android.R.attr.textColorTertiary); // Needed to draw folder leave-behind when opening one. setWillNotDraw(false); - mAllAppsButton = (IconButtonView) LayoutInflater.from(context) - .inflate(R.layout.taskbar_all_apps_button, this, false); - mAllAppsButton.setIconDrawable(resources.getDrawable( - getAllAppsButton(isTransientTaskbar))); - mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); - mAllAppsButton.setForegroundTint( - mActivityContext.getColor(R.color.all_apps_button_color)); + mAllAppsButtonContainer = new TaskbarAllAppsButtonContainer(context); + mAllAppsButtonTranslationOffset = (int) getResources().getDimension( + mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset( + mActivityContext.isTransientTaskbar())); - if (enableTaskbarPinning()) { - mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate( - R.layout.taskbar_divider, - this, false); - mTaskbarDivider.setIconDrawable( - resources.getDrawable(R.drawable.taskbar_divider_button)); - mTaskbarDivider.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); + if (enableTaskbarPinning() || enableRecentsInTaskbar()) { + mTaskbarDividerContainer = new TaskbarDividerContainer(context); + } + + if (ENABLE_TASKBAR_OVERFLOW.isTrue()) { + mTaskbarOverflowView = TaskbarOverflowView.inflateIcon( + R.layout.taskbar_overflow_view, this, + mIconTouchSize, mItemPadding); } // TODO: Disable touch events on QSB otherwise it can crash. @@ -194,35 +225,92 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } else { mQsb = LayoutInflater.from(context).inflate(R.layout.empty_view, this, false); } - - // Default long press (touch) delay = 400ms - mAllAppsButtonTouchDelayMs = ViewConfiguration.getLongPressTimeout(); } - @DrawableRes - private int getAllAppsButton(boolean isTransientTaskbar) { - boolean shouldSelectTransientIcon = (isTransientTaskbar || enableTaskbarPinning()) - && !mActivityContext.isThreeButtonNav(); - if (ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) { - return shouldSelectTransientIcon - ? R.drawable.ic_transient_taskbar_all_apps_search_button - : R.drawable.ic_taskbar_all_apps_search_button; - } else { - return shouldSelectTransientIcon - ? R.drawable.ic_transient_taskbar_all_apps_button - : R.drawable.ic_taskbar_all_apps_button; + /** + * @return the maximum number of 'icons' that can fit in the taskbar. + */ + private int calculateMaxNumIcons() { + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + int availableWidth = deviceProfile.getDeviceProperties().getWidthPx(); + int defaultEdgeMargin = + (int) getResources().getDimension(deviceProfile.inv.inlineNavButtonsEndSpacing); + int spaceForBubbleBar = + Math.round(mControllerCallbacks.getBubbleBarMaxCollapsedWidthIfVisible()); + + // Reserve space required for edge margins, or for navbar if shown. If task bar needs to be + // center aligned with nav bar shown, reserve space on both sides. + availableWidth -= Math.max( + defaultEdgeMargin + spaceForBubbleBar, + deviceProfile.getHotseatProfile().getBarEndOffset()); + availableWidth -= Math.max( + defaultEdgeMargin + (mShouldTryStartAlign ? 0 : spaceForBubbleBar), + mShouldTryStartAlign ? 0 : deviceProfile.getHotseatProfile().getBarEndOffset()); + + // The space taken by an item icon used during layout. + int iconSize = 2 * mItemMarginLeftRight + mIconTouchSize; + + int additionalIcons = 0; + + if (mTaskbarDividerContainer != null) { + // Space for divider icon is reduced during layout compared to normal icon size, reserve + // space for the divider separately. + availableWidth -= iconSize - 4 * mItemMarginLeftRight; + ++additionalIcons; } + + // All apps icon takes less space compared to normal icon size, reserve space for the icon + // separately. + boolean forceTransientTaskbarSize = + enableTaskbarPinning() && canTransitionToTransientTaskbar(); + availableWidth -= iconSize - (int) getResources().getDimension( + mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset( + forceTransientTaskbarSize || mActivityContext.isTransientTaskbar())); + ++additionalIcons; + + return Math.floorDiv(availableWidth, iconSize) + additionalIcons; } - @DimenRes - public int getAllAppsButtonTranslationXOffset(boolean isTransientTaskbar) { - if (isTransientTaskbar) { - return R.dimen.transient_taskbar_all_apps_button_translation_x_offset; - } else { - return ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get() - ? R.dimen.taskbar_all_apps_search_button_translation_x_offset - : R.dimen.taskbar_all_apps_button_translation_x_offset; + /** + * Whether the taskbar in the state context supports transition to a transient taskbar (e.g. + * using a popup menu). + */ + boolean canTransitionToTransientTaskbar() { + return mActivityContext.getTaskbarFeatureEvaluator() + .getSupportsTransitionToTransientTaskbar(); + } + + /** + * Recalculates the max number of icons the taskbar view can show without entering overflow. + * Returns whether the max number of icons changed and the change affects the number of icons + * that should be shown in the taskbar. + */ + boolean updateMaxNumIcons() { + if (!ENABLE_TASKBAR_OVERFLOW.isTrue()) { + return false; } + int oldMaxNumIcons = mMaxNumIcons; + mMaxNumIcons = calculateMaxNumIcons(); + return oldMaxNumIcons != mMaxNumIcons + && (mIdealNumIcons > oldMaxNumIcons || mIdealNumIcons > mMaxNumIcons); + } + + /** + * Pre-adds views that are always children of this view for LayoutTransition support. + *

+ * Normally these views are removed and re-added when updating hotseat and recents. This + * approach does not behave well with LayoutTransition, so we instead need to add them + * initially and avoid removing them during updates. + */ + private int addStaticViews() { + int numStaticViews = 1; + addView(mAllAppsButtonContainer); + if (mActivityContext.getDeviceProfile().isQsbInline) { + addView(mQsb, mIsRtl ? 1 : 0); + mQsb.setVisibility(View.INVISIBLE); + numStaticViews++; + } + return numStaticViews; } @Override @@ -248,26 +336,28 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar @Override public void onDeviceProfileChanged(DeviceProfile dp) { - mShouldTryStartAlign = mActivityContext.isThreeButtonNav() && dp.startAlignTaskbar; + mShouldTryStartAlign = mActivityContext.shouldStartAlignTaskbar(); } - @Override - public boolean performAccessibilityActionInternal(int action, Bundle arguments) { - if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) { + private void announceTaskbarShown() { + BubbleBarLocation bubbleBarLocation = mControllerCallbacks.getBubbleBarLocationIfVisible(); + if (bubbleBarLocation == null) { announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title)); - } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) { - announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title)); + } else if (bubbleBarLocation.isOnLeft(isLayoutRtl())) { + announceForAccessibility( + mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_left_title)); + } else { + announceForAccessibility( + mContext.getString(R.string.taskbar_a11y_shown_with_bubbles_right_title)); } - return super.performAccessibilityActionInternal(action, arguments); - } protected void announceAccessibilityChanges() { - this.performAccessibilityAction( - isVisibleToUser() ? AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS - : AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, - null); - + // Only announce taskbar window shown. Window disappearing is generally not announce. + // This also aligns with talkback guidelines and unnecessary announcement to users. + if (isVisibleToUser()) { + announceTaskbarShown(); + } ActivityContext.lookupContext(getContext()).getDragLayer() .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED); } @@ -288,27 +378,33 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mIconClickListener = mControllerCallbacks.getIconOnClickListener(); mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener(); - if (mAllAppsButton != null) { - mAllAppsButton.setOnClickListener(this::onAllAppsButtonClick); - mAllAppsButton.setOnLongClickListener(this::onAllAppsButtonLongClick); - mAllAppsButton.setOnTouchListener(this::onAllAppsButtonTouch); - mAllAppsButton.setHapticFeedbackEnabled( - mControllerCallbacks.isAllAppsButtonHapticFeedbackEnabled()); - mAllAppsTouchRunnable = () -> { - mControllerCallbacks.triggerAllAppsButtonLongClick(); - mAllAppsTouchTriggered = true; - }; - AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(mContext); - if (DeviceConfigWrapper.get().getCustomLpaaThresholds() - && assistStateManager.getLPNHDurationMillis().isPresent()) { - mAllAppsButtonTouchDelayMs = assistStateManager.getLPNHDurationMillis().get(); + mAllAppsButtonContainer.setUpCallbacks(callbacks); + if (mTaskbarOverflowView != null) { + mTaskbarOverflowView.setOnClickListener( + mControllerCallbacks.getOverflowOnClickListener()); + mTaskbarOverflowView.setOnLongClickListener( + mControllerCallbacks.getOverflowOnLongClickListener()); + if (enableCursorHoverStates()) { + setHoverListenerForIcon(mTaskbarOverflowView); } } - if (mTaskbarDivider != null && !mActivityContext.isThreeButtonNav()) { - mTaskbarDivider.setOnLongClickListener( - mControllerCallbacks.getTaskbarDividerLongClickListener()); - mTaskbarDivider.setOnTouchListener( - mControllerCallbacks.getTaskbarDividerRightClickListener()); + + if (ENABLE_TASKBAR_OVERFLOW.isTrue()) { + mMaxNumIcons = calculateMaxNumIcons(); + } + } + + void updatePinningPopupEventHandlers() { + boolean supportsPinningPopup = + mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup(); + if (mTaskbarDividerContainer != null) { + mTaskbarDividerContainer.setUpCallbacks( + supportsPinningPopup ? mControllerCallbacks : null); + } + + if (Flags.showTaskbarPinningPopupFromAnywhere()) { + setOnTouchListener( + supportsPinningPopup ? mControllerCallbacks.getTaskbarTouchListener() : null); } } @@ -322,31 +418,209 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar view.setTag(null); } - /** - * Inflates/binds the Hotseat views to show in the Taskbar given their - * ItemInfos. - */ - protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) { - int nextViewIndex = 0; - int numViewsAnimated = 0; + /** Inflates/binds the hotseat items and recent tasks to the view. */ + protected void updateItems(ItemInfo[] hotseatItemInfos, List recentTasks) { + if (mActivityContext.isDestroyed()) return; + // Filter out unsupported items. + hotseatItemInfos = Arrays.stream(hotseatItemInfos) + .filter(Objects::nonNull) + .toArray(ItemInfo[]::new); + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. + recentTasks = recentTasks.stream().filter(it -> it instanceof SingleTask).toList(); - if (mAllAppsButton != null) { - removeView(mAllAppsButton); + if (ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue()) { + updateItemsWithLayoutTransition(hotseatItemInfos, recentTasks); + } else { + updateItemsWithoutLayoutTransition(hotseatItemInfos, recentTasks); + } + mAllAppsButtonContainer.updateTaskbarMinimalState(isTaskbarInMinimalState()); + } - if (mTaskbarDivider != null) { - removeView(mTaskbarDivider); - } + public boolean isTaskbarInMinimalState() { + return getIconViews().length <= 1; + } + + private void updateItemsWithoutLayoutTransition( + ItemInfo[] hotseatItemInfos, List recentTasks) { + + mNextViewIndex = 0; + mAddedDividerForRecents = false; + + removeView(mAllAppsButtonContainer); + + if (mTaskbarDividerContainer != null) { + removeView(mTaskbarDividerContainer); + } + if (mTaskbarOverflowView != null) { + removeView(mTaskbarOverflowView); } removeView(mQsb); - for (int i = 0; i < hotseatItemInfos.length; i++) { - ItemInfo hotseatItemInfo = hotseatItemInfos[i]; - if (hotseatItemInfo == null) { - continue; + mIgnoreTaskbarIconCount = getIgnoreCountForTaskbarIcons(recentTasks.size(), + hotseatItemInfos.length); + + updateHotseatItems(hotseatItemInfos); + + if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { + addView(mTaskbarDividerContainer, mNextViewIndex++); + mAddedDividerForRecents = true; + } + + updateRecents(recentTasks, hotseatItemInfos.length); + + addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0); + + // If there are no recent tasks, add divider after All Apps (unless it's the only view). + if (!mAddedDividerForRecents + && mTaskbarDividerContainer != null + && getChildCount() > 1) { + addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1); + } + + if (mActivityContext.getDeviceProfile().isQsbInline) { + addView(mQsb, mIsRtl ? getChildCount() : 0); + // Always set QSB to invisible after re-adding. + mQsb.setVisibility(View.INVISIBLE); + } + } + + private void updateItemsWithLayoutTransition( + ItemInfo[] hotseatItemInfos, List recentTasks) { + if (mNumStaticViews == 0) { + mNumStaticViews = addStaticViews(); + } + + // Skip static views and potential All Apps divider, if they are on the left. + mNextViewIndex = mIsRtl ? 0 : mNumStaticViews; + if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer && !mAddedDividerForRecents) { + mNextViewIndex++; + } + + mIgnoreTaskbarIconCount = getIgnoreCountForTaskbarIcons(recentTasks.size(), + hotseatItemInfos.length); + + // Update left section. + if (mIsRtl) { + updateRecents(recentTasks.reversed(), hotseatItemInfos.length); + } else { + updateHotseatItems(hotseatItemInfos); + } + + // Now at theoretical position for recent apps divider. + updateRecentsDivider(!recentTasks.isEmpty()); + if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer) { + mNextViewIndex++; + } + + // Update right section. + if (mIsRtl) { + updateHotseatItems(hotseatItemInfos); + } else { + updateRecents(recentTasks, hotseatItemInfos.length); + } + + // Recents divider takes priority. + if (!mAddedDividerForRecents) { + boolean allAppsDividerAllowed = !mActivityContext.isTaskbarShowingDesktopTasks(); + if (allAppsDividerAllowed) { + updateAllAppsDivider(); + } else if (getChildAt(getExpectedAllAppsDividerIndex()) == mTaskbarDividerContainer) { + removeView(mTaskbarDividerContainer); } - // Replace any Hotseat views with the appropriate type if it's not already that - // type. + } + } + + private void updateRecentsDivider(boolean hasRecents) { + if (hasRecents && !mAddedDividerForRecents) { + mAddedDividerForRecents = true; + + // Remove possible All Apps divider. + if (getChildAt(mNumStaticViews) == mTaskbarDividerContainer) { + mNextViewIndex--; // All Apps divider on the left. Need to account for removing it. + } + removeView(mTaskbarDividerContainer); + + addView(mTaskbarDividerContainer, mNextViewIndex); + } else if (!hasRecents && mAddedDividerForRecents) { + mAddedDividerForRecents = false; + removeViewAt(mNextViewIndex); + } + } + + private void updateAllAppsDivider() { + // Index where All Apps divider would be if it is already in Taskbar. + final int expectedAllAppsDividerIndex = getExpectedAllAppsDividerIndex(); + if (getChildAt(expectedAllAppsDividerIndex) == mTaskbarDividerContainer + && getChildCount() == mNumStaticViews + 1) { + // Only static views with divider so remove divider. + removeView(mTaskbarDividerContainer); + } else if (getChildAt(expectedAllAppsDividerIndex) != mTaskbarDividerContainer + && getChildCount() >= mNumStaticViews + 1) { + // Static views with at least one app icon so add divider. For RTL, add it after the + // icon that is at the expected index. + addView( + mTaskbarDividerContainer, + mIsRtl ? expectedAllAppsDividerIndex + 1 : expectedAllAppsDividerIndex); + } + } + + private int getExpectedAllAppsDividerIndex() { + return mIsRtl ? getChildCount() - mNumStaticViews - 1 : mNumStaticViews; + } + + /** + * Calculate how many icon we need to not show in Taskbar that are present in hotseat. + */ + private int getIgnoreCountForTaskbarIcons(int recentsIcons, int hotseatIcons) { + + if (!mActivityContext.isThreeButtonNav() + || mActivityContext.getTaskbarFeatureEvaluator().isRecentsEnabled()) { + return 0; + } + + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + + // Add icon for all apps. + int icons = 1; + + // Only include divider line in count if will be added to Taskbar view which is in + // conditions below. + if (mActivityContext.isInDesktopMode() && recentsIcons > 0) { + icons += 1; + } else if (recentsIcons + hotseatIcons != 0) { + icons += 1; + } + int spaceNeeded = getIconLayoutWidth(icons + recentsIcons + hotseatIcons); + + boolean areBubblesVisible = + mControllerCallbacks.isBubbleBarEnabled() && mBubbleBarLocation != null; + int screenWidth = this.getResources().getDisplayMetrics().widthPixels; + int navSpaceNeeded = deviceProfile.getHotseatProfile().getBarEndOffset(); + + int ignoreCount = 0; + //Screen Width - nav space + int amountOfSpaceTaskbarIconsCanHave = screenWidth - navSpaceNeeded; + if (areBubblesVisible) { + // size of bubbles Icon and margin on the side. + int bubbleBarMargin = getResources().getDimensionPixelSize( + R.dimen.transient_taskbar_bottom_margin); + amountOfSpaceTaskbarIconsCanHave -= (mIconTouchSize + bubbleBarMargin); + } + int taskbarIconSpaceNeeded = spaceNeeded; + while (amountOfSpaceTaskbarIconsCanHave < taskbarIconSpaceNeeded) { + ignoreCount++; + int iconSpace = mIconTouchSize + (2 * mItemMarginLeftRight); + taskbarIconSpaceNeeded -= iconSpace; + } + return ignoreCount; + } + + private void updateHotseatItems(ItemInfo[] hotseatItemInfos) { + int numViewsAnimated = 0; + + for (ItemInfo hotseatItemInfo : hotseatItemInfos) { + // Replace any Hotseat views with the appropriate type if it's not already that type. final int expectedLayoutResId; boolean isCollection = false; if (hotseatItemInfo.isPredictedItem()) { @@ -361,8 +635,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } View hotseatView = null; - while (nextViewIndex < getChildCount()) { - hotseatView = getChildAt(nextViewIndex); + while (isNextViewInSection(ItemInfo.class)) { + hotseatView = getChildAt(mNextViewIndex); // see if the view can be reused if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId) @@ -401,42 +675,190 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } else { hotseatView = inflate(expectedLayoutResId); } - LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize); + LayoutParams lp = new TaskbarLayoutParams(mIconTouchSize, mIconTouchSize); hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); - addView(hotseatView, nextViewIndex, lp); + addView(hotseatView, mNextViewIndex, lp); + } else if (hotseatView instanceof FolderIcon fi) { + fi.onItemsChanged(false); + fi.getFolder().reapplyItemInfo(); } - // Apply the Hotseat ItemInfos, or hide the view if there is none for a given - // index. - if (hotseatView instanceof BubbleTextView - && hotseatItemInfo instanceof WorkspaceItemInfo) { - BubbleTextView btv = (BubbleTextView) hotseatView; - WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo; + if (hotseatView.getLayoutParams() instanceof TaskbarLayoutParams tlp) { + tlp.bindInfo = new CellInfo(hotseatView, + hotseatItemInfo.screenId, hotseatItemInfo.container, + hotseatItemInfo.cellX, hotseatItemInfo.cellY, + hotseatItemInfo.spanX, hotseatItemInfo.spanY); + } - boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo); - btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated); - if (animate) { - numViewsAnimated++; + // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index. + if (hotseatView instanceof BubbleTextView btv + && hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) { + if (btv instanceof PredictedAppIcon pai) { + if (pai.applyFromWorkspaceItemWithAnimation(workspaceInfo, numViewsAnimated)) { + numViewsAnimated++; + } + } else { + btv.applyFromWorkspaceItem(workspaceInfo); } } setClickAndLongClickListenersForIcon(hotseatView); if (enableCursorHoverStates()) { setHoverListenerForIcon(hotseatView); } - nextViewIndex++; - } - // Remove remaining views - while (nextViewIndex < getChildCount()) { - removeAndRecycle(getChildAt(nextViewIndex)); + mNextViewIndex++; } - if (mAllAppsButton != null) { + while (isNextViewInSection(ItemInfo.class)) { + removeAndRecycle(getChildAt(mNextViewIndex)); } - if (mActivityContext.getDeviceProfile().isQsbInline) { - addView(mQsb, mIsRtl ? getChildCount() : 0); - // Always set QSB to invisible after re-adding. - mQsb.setVisibility(View.INVISIBLE); + } + + private void updateRecents(List recentTasks, int hotseatSize) { + boolean supportsOverflow = ENABLE_TASKBAR_OVERFLOW.isTrue() && recentTasks.size() > 1; + int overflowSize = 0; + boolean hasOverflow = false; + if (supportsOverflow && mTaskbarOverflowView != null) { + // Need to account for All Apps and the divider. If we need to have an overflow, we will + // have a divider for recents. + final int nonTaskIconsToBeAdded = 2; + mIdealNumIcons = hotseatSize + recentTasks.size() + nonTaskIconsToBeAdded; + overflowSize = mIdealNumIcons - mMaxNumIcons; + hasOverflow = overflowSize > 0; + + if (!ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() && hasOverflow) { + addView(mTaskbarOverflowView, mNextViewIndex++); + } else if (ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue()) { + // RTL case is handled after we add the recent icons, because the button needs to + // then be to the right of them. + if (hasOverflow && !mIsRtl) { + if (mPrevOverflowTasks.isEmpty()) addView(mTaskbarOverflowView, mNextViewIndex); + // NOTE: If overflow already existed, assume the overflow view is already + // at the correct position. + mNextViewIndex++; + } else if (!hasOverflow && !mPrevOverflowTasks.isEmpty()) { + removeView(mTaskbarOverflowView); + mTaskbarOverflowView.clearItems(); + } + } else { + mTaskbarOverflowView.clearItems(); + } } + + // An extra item needs to be added to overflow button to account for the space taken up by + // the overflow button. + final int itemsToAddToOverflow = + hasOverflow ? Math.min(overflowSize + 1, recentTasks.size()) : 0; + final Set overflownRecentsSet; + if (hasOverflow && mTaskbarOverflowView != null) { + final int startIndex = mIsRtl ? recentTasks.size() - itemsToAddToOverflow : 0; + final int endIndex = mIsRtl ? recentTasks.size() : itemsToAddToOverflow; + final List overflownRecents = recentTasks.subList(startIndex, endIndex); + mTaskbarOverflowView.setItems( + overflownRecents.stream().map(t -> ((SingleTask) t).getTask()).toList()); + overflownRecentsSet = new ArraySet<>(overflownRecents); + } else { + overflownRecentsSet = Collections.emptySet(); + } + + // Add Recent/Running icons. + final Set recentTasksSet = new ArraySet<>(recentTasks); + final int startIndex = mIsRtl ? 0 : itemsToAddToOverflow; + final int endIndex = + mIsRtl ? recentTasks.size() - itemsToAddToOverflow : recentTasks.size(); + for (GroupTask task : recentTasks.subList(startIndex, endIndex)) { + // Replace any Recent views with the appropriate type if it's not already that type. + final int expectedLayoutResId; + boolean isCollection = false; + if (!(task instanceof SingleTask)) { + if (task.taskViewType == TaskViewType.DESKTOP) { + // TODO(b/316004172): use Desktop tile layout. + expectedLayoutResId = -1; + } else { + // TODO(b/343289567): use R.layout.app_pair_icon + expectedLayoutResId = -1; + } + isCollection = true; + } else { + expectedLayoutResId = R.layout.taskbar_app_icon; + } + + View recentIcon = null; + // If a task is new, we should not reuse a view so that it animates in when it is added. + final boolean canReuseView = !ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() + || (mPrevRecentTasks.contains(task) && !mPrevOverflowTasks.contains(task)); + while (canReuseView && isNextViewInSection(GroupTask.class)) { + recentIcon = getChildAt(mNextViewIndex); + GroupTask tag = (GroupTask) recentIcon.getTag(); + + // see if the view can be reused + if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId) + || (isCollection && tag != task) + // Remove view corresponding to removed task so that it animates out. + || (ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() + && (!recentTasksSet.contains(tag) + || overflownRecentsSet.contains(tag)))) { + removeAndRecycle(recentIcon); + recentIcon = null; + } else { + // View found + break; + } + } + + if (recentIcon == null) { + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. + recentIcon = inflate(expectedLayoutResId); + LayoutParams lp = new TaskbarLayoutParams(mIconTouchSize, mIconTouchSize); + recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); + addView(recentIcon, mNextViewIndex, lp); + } + + if (recentIcon instanceof BubbleTextView btv) { + applyGroupTaskToBubbleTextView(btv, task); + } + setClickAndLongClickListenersForIcon(recentIcon); + if (enableCursorHoverStates()) { + setHoverListenerForIcon(recentIcon); + } + mNextViewIndex++; + } + + while (isNextViewInSection(GroupTask.class)) { + removeAndRecycle(getChildAt(mNextViewIndex)); + } + + if (ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() && mIsRtl && hasOverflow) { + if (mPrevOverflowTasks.isEmpty()) { + addView(mTaskbarOverflowView, mNextViewIndex); + } + mNextViewIndex++; + } + + mPrevRecentTasks = recentTasksSet; + mPrevOverflowTasks = overflownRecentsSet; + } + + private boolean isNextViewInSection(Class tagClass) { + return mNextViewIndex < getChildCount() + && tagClass.isInstance(getChildAt(mNextViewIndex).getTag()); + } + + /** Binds the SingleTask to the BubbleTextView to be ready to present to the user. */ + public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) { + if (!(groupTask instanceof SingleTask singleTask)) { + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. + return; + } + + Task task = singleTask.getTask(); + // TODO(b/344038728): use FastBitmapDrawable instead of Drawable, to get disabled state + // while dragging. + Drawable taskIcon = task.icon; + if (taskIcon != null) { + taskIcon = taskIcon.getConstantState().newDrawable().mutate(); + } + btv.applyIconAndLabel(taskIcon, task.title, task.titleDescription); + btv.setTag(singleTask); } /** @@ -464,55 +886,84 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar icon.setOnHoverListener(mControllerCallbacks.getIconOnHoverListener(icon)); } + /** Updates taskbar icons accordingly to the new bubble bar location. */ + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + if (mBubbleBarLocation == location) return; + mBubbleBarLocation = location; + requestLayout(); + } + + /** + * Returns translation X for the taskbar icons for provided {@link BubbleBarLocation}. If the + * bubble bar is not enabled, or location of the bubble bar is the same, or taskbar is not start + * aligned - returns 0. + */ + public float getTranslationXForBubbleBarPosition(BubbleBarLocation location) { + if (!mControllerCallbacks.isBubbleBarEnabled() + || location == mBubbleBarLocation + || !mActivityContext.shouldStartAlignTaskbar() + ) { + return 0; + } + Rect iconsBounds = getTransientTaskbarIconLayoutBoundsInParent(); + + int translateXFromIgnoredIcons = + mIgnoreTaskbarIconCount * (mIconTouchSize + mItemMarginLeftRight); + // If bubble bar or right translate in opposite direction. + if (!location.isOnLeft(isLayoutRtl())) { + translateXFromIgnoredIcons *= -1; + } + return getTaskBarIconsEndForBubbleBarLocation(location) - iconsBounds.right + + translateXFromIgnoredIcons; + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int count = getChildCount(); - DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); int spaceNeeded = getIconLayoutWidth(); - int navSpaceNeeded = deviceProfile.hotseatBarEndOffset; boolean layoutRtl = isLayoutRtl(); - int centerAlignIconEnd = right - (right - left - spaceNeeded) / 2; - int iconEnd; - + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + int navSpaceNeeded = deviceProfile.getHotseatProfile().getBarEndOffset(); + int centerAlignIconEnd = (right + left + spaceNeeded) / 2; + int iconEnd = centerAlignIconEnd; if (mShouldTryStartAlign) { - // Taskbar is aligned to the start - int startSpacingPx = deviceProfile.inlineNavButtonsEndSpacingPx; - - if (layoutRtl) { - iconEnd = right - startSpacingPx; + int startSpacingPx = + deviceProfile.getHotseatProfile().getInlineNavButtonsEndSpacingPx(); + if (mControllerCallbacks.isBubbleBarEnabled() + && mBubbleBarLocation != null + && mActivityContext.shouldStartAlignTaskbar()) { + iconEnd = (int) getTaskBarIconsEndForBubbleBarLocation(mBubbleBarLocation); } else { - iconEnd = startSpacingPx + spaceNeeded; + if (layoutRtl) { + iconEnd = right - startSpacingPx; + } else { + iconEnd = startSpacingPx + spaceNeeded; + } + boolean needMoreSpaceForNav = layoutRtl + ? navSpaceNeeded > (iconEnd - spaceNeeded) + : iconEnd > (right - navSpaceNeeded); + if (needMoreSpaceForNav) { + // Add offset to account for nav bar when taskbar is centered + int offset = layoutRtl + ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded) + : (right - navSpaceNeeded) - centerAlignIconEnd; + iconEnd = centerAlignIconEnd + offset; + } } - } else { - iconEnd = centerAlignIconEnd; } - boolean needMoreSpaceForNav = layoutRtl - ? navSpaceNeeded > (iconEnd - spaceNeeded) - : iconEnd > (right - navSpaceNeeded); - if (needMoreSpaceForNav) { - // Add offset to account for nav bar when taskbar is centered - int offset = layoutRtl - ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded) - : (right - navSpaceNeeded) - centerAlignIconEnd; - - iconEnd = centerAlignIconEnd + offset; - } - - // Currently, we support only one device with display cutout and we only are - // concern about + // Currently, we support only one device with display cutout and we only are concern about // it when the bottom rect is present and non empty DisplayCutout displayCutout = getDisplay().getCutout(); if (displayCutout != null && !displayCutout.getBoundingRectBottom().isEmpty()) { Rect cutoutBottomRect = displayCutout.getBoundingRectBottom(); - // when cutout present at the bottom of screen align taskbar icons to cutout - // offset + // when cutout present at the bottom of screen align taskbar icons to cutout offset // if taskbar icon overlaps with cutout int taskbarIconLeftBound = iconEnd - spaceNeeded; int taskbarIconRightBound = iconEnd; - boolean doesTaskbarIconsOverlapWithCutout = taskbarIconLeftBound <= cutoutBottomRect.centerX() - && cutoutBottomRect.centerX() <= taskbarIconRightBound; + boolean doesTaskbarIconsOverlapWithCutout = + taskbarIconLeftBound <= cutoutBottomRect.centerX() + && cutoutBottomRect.centerX() <= taskbarIconRightBound; if (doesTaskbarIconsOverlapWithCutout) { if (!layoutRtl) { @@ -529,6 +980,28 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mIconLayoutBounds.right = iconEnd; mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2; mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize; + + // With rtl layout, the all apps button will be translated by `allAppsButtonOffset` after + // layout completion (by `TaskbarViewController`). Offset the icon end by the same amount + // when laying out icons, so the taskbar content remains centered after all apps button + // translation. + if (layoutRtl) { + iconEnd += mAllAppsButtonTranslationOffset; + } + + if (mActivityContext.isThreeButtonNav()) { + boolean navbarOnLeft = mBubbleBarLocation != null && !mBubbleBarLocation.isOnLeft( + layoutRtl); + if (navbarOnLeft && layoutRtl) { + iconEnd -= (mIconTouchSize + mItemMarginLeftRight) * mIgnoreTaskbarIconCount; + } else if (!navbarOnLeft && !layoutRtl) { + iconEnd += (mIconTouchSize + mItemMarginLeftRight) * mIgnoreTaskbarIconCount; + } + } + + mControllerCallbacks.onPreLayoutChildren(); + + int count = getChildCount(); for (int i = count; i > 0; i--) { View child = getChildAt(i - 1); if (child == mQsb) { @@ -541,10 +1014,10 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar qsbEnd = iconEnd - mItemMarginLeftRight; qsbStart = qsbEnd - deviceProfile.hotseatQsbWidth; } - int qsbTop = (bottom - top - deviceProfile.hotseatQsbHeight) / 2; - int qsbBottom = qsbTop + deviceProfile.hotseatQsbHeight; + int qsbTop = (bottom - top - deviceProfile.getHotseatProfile().getQsbHeight()) / 2; + int qsbBottom = qsbTop + deviceProfile.getHotseatProfile().getQsbHeight(); child.layout(qsbStart, qsbTop, qsbEnd, qsbBottom); - } else if (child == mTaskbarDivider) { + } else if (child == mTaskbarDividerContainer) { iconEnd += mItemMarginLeftRight; int iconStart = iconEnd - mIconTouchSize; child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom); @@ -559,6 +1032,15 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mIconLayoutBounds.left = iconEnd; + // Adjust the icon layout bounds by the amount by which all apps button will be translated + // post layout to maintain margin between all apps button and the edge of the transient + // taskbar background. Done for ltr layout only - for rtl layout, the offset needs to be + // adjusted on the right, which is done by offsetting `iconEnd` after setting + // `mIconLayoutBounds.right`. + if (!layoutRtl) { + mIconLayoutBounds.left += mAllAppsButtonTranslationOffset; + } + if (mIconLayoutBounds.right - mIconLayoutBounds.left < mTransientTaskbarMinWidth) { int center = mIconLayoutBounds.centerX(); int distanceFromCenter = (int) mTransientTaskbarMinWidth / 2; @@ -572,66 +1054,139 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } /** - * Returns whether the given MotionEvent, *in screen coorindates*, is within any - * Taskbar item's + * Returns whether the given MotionEvent, *in screen coordinates*, is within any Taskbar item's * touch bounds. */ public boolean isEventOverAnyItem(MotionEvent ev) { getLocationOnScreen(mTempOutLocation); - int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0]; - int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1]; - return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates); - } - - public Rect getIconLayoutBounds() { - return mIconLayoutBounds; + int xInOurCoordinates = (int) ev.getRawX() - mTempOutLocation[0]; + int yInOurCoordinates = (int) ev.getRawY() - mTempOutLocation[1]; + return isShown() && getTaskbarIconsActualBounds().contains(xInOurCoordinates, + yInOurCoordinates); } /** - * Returns the space used by the icons + * Returns the current visual taskbar icons bounds (unlike `mIconLayoutBounds` which contains + * bounds for transient mode only). */ - public int getIconLayoutWidth() { - int countExcludingQsb = getChildCount(); + private Rect getTaskbarIconsActualBounds() { + View[] iconViews = getIconViews(); + if (iconViews.length == 0) { + return new Rect(); + } + + int[] firstIconViewLocation = new int[2]; + int[] lastIconViewLocation = new int[2]; + iconViews[0].getLocationOnScreen(firstIconViewLocation); + iconViews[iconViews.length - 1].getLocationOnScreen(lastIconViewLocation); + + return new Rect(firstIconViewLocation[0], 0, lastIconViewLocation[0] + mIconTouchSize, + getHeight()); + } + + /** + * Gets visual bounds of the taskbar view. The visual bounds correspond to the taskbar touch + * area, rather than layout placement in the parent view. + */ + public Rect getTransientTaskbarIconLayoutBounds() { + return new Rect(mIconLayoutBounds); + } + + /** Gets taskbar layout bounds in parent view. */ + public Rect getTransientTaskbarIconLayoutBoundsInParent() { + Rect actualBounds = new Rect(mIconLayoutBounds); + actualBounds.top = getTop(); + actualBounds.bottom = getBottom(); + return actualBounds; + } + + /** + * Returns the space used by the icons. + */ + private int getIconLayoutWidth() { + return getIconLayoutWidth(getChildCount()); + } + + /** + * Return the space needed based on the number of taskbar icons supplied vs existing children. + */ + private int getIconLayoutWidth(int expectedNumberOfTaskbarIcons) { + int countExcludingQsb = expectedNumberOfTaskbarIcons; DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); if (deviceProfile.isQsbInline) { countExcludingQsb--; } - int iconLayoutBoundsWidth = countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize); + + int iconLayoutBoundsWidth = + countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize); if (enableTaskbarPinning() && countExcludingQsb > 1) { // We are removing 4 * mItemMarginLeftRight as there should be no space between // All Apps icon, divider icon, and first app icon in taskbar iconLayoutBoundsWidth -= mItemMarginLeftRight * 4; } + + // The all apps button container gets offset horizontally, reducing the overall taskbar + // view size. + iconLayoutBoundsWidth -= mAllAppsButtonTranslationOffset; + return iconLayoutBoundsWidth; } /** - * Returns the app icons currently shown in the taskbar. + * Returns the app icons currently shown in the taskbar. The returned list does not include qsb, + * but it includes all apps button and icon divider views. */ public View[] getIconViews() { final int count = getChildCount(); - View[] icons = new View[count]; + if (count == 0) { + return new View[0]; + } + View[] icons = new View[count - (mActivityContext.getDeviceProfile().isQsbInline ? 1 : 0)]; + int insertionPoint = 0; for (int i = 0; i < count; i++) { - icons[i] = getChildAt(i); + if (getChildAt(i) == mQsb) continue; + icons[insertionPoint++] = getChildAt(i); } return icons; } + /** + * The max number of icon views the taskbar can have when taskbar overflow is enabled. + */ + int getMaxNumIconViews() { + return mMaxNumIcons; + } + /** * Returns the all apps button in the taskbar. */ - @Nullable - public View getAllAppsButtonView() { - return mAllAppsButton; + public TaskbarAllAppsButtonContainer getAllAppsButtonContainer() { + return mAllAppsButtonContainer; } /** * Returns the taskbar divider in the taskbar. */ @Nullable - public View getTaskbarDividerView() { - return mTaskbarDivider; + public TaskbarDividerContainer getTaskbarDividerViewContainer() { + return mTaskbarDividerContainer; + } + + /** + * Returns the taskbar overflow view in the taskbar. + */ + @Nullable + public TaskbarOverflowView getTaskbarOverflowView() { + return mTaskbarOverflowView; + } + + /** + * Returns whether the divider is between Hotseat icons and Recents, + * instead of between All Apps button and Hotseat. + */ + public boolean isDividerForRecents() { + return mAddedDividerForRecents; } /** @@ -671,6 +1226,12 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } + @Override + protected void dispatchDraw(Canvas canvas) { + if (mActivityContext.isDestroyed()) return; + super.dispatchDraw(canvas); + } + private View inflate(@LayoutRes int layoutResId) { return mActivityContext.getViewCache().getView(layoutResId, mActivityContext, this); } @@ -686,81 +1247,57 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } /** - * Maps {@code op} over all the child views. + * @return The all apps button horizontal offset used to calculate the taskbar contents width + * during layout. */ - public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) { - // map over all the shortcuts on the taskbar - for (int i = 0; i < getChildCount(); i++) { - View item = getChildAt(i); - if (op.evaluate((ItemInfo) item.getTag(), item)) { - return; - } - } + public int getAllAppsButtonTranslationXOffsetUsedForLayout() { + return mAllAppsButtonTranslationOffset; } /** - * Finds the first icon to match one of the given matchers, from highest to - * lowest priority. - * - * @return The first match, or All Apps button if no match was found. + * This method only works for bubble bar enabled in persistent task bar and the taskbar is start + * aligned. */ - public View getFirstMatch(Predicate... matchers) { - for (Predicate matcher : matchers) { - for (int i = 0; i < getChildCount(); i++) { - View item = getChildAt(i); - if (!(item.getTag() instanceof ItemInfo)) { - // Should only happen for All Apps button. - continue; - } - ItemInfo info = (ItemInfo) item.getTag(); - if (matcher.test(info)) { - return item; - } - } - } - return mAllAppsButton; - } - - private boolean onAllAppsButtonTouch(View view, MotionEvent ev) { - switch (ev.getAction()) { - case MotionEvent.ACTION_DOWN: - mAllAppsTouchTriggered = false; - MAIN_EXECUTOR.getHandler().postDelayed( - mAllAppsTouchRunnable, mAllAppsButtonTouchDelayMs); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - cancelAllAppsButtonTouch(); - } - return false; - } - - private void cancelAllAppsButtonTouch() { - MAIN_EXECUTOR.getHandler().removeCallbacks(mAllAppsTouchRunnable); - // ACTION_UP is first triggered, then click listener / long-click listener is - // triggered on - // the next frame, so we need to post twice and delay the reset. - if (mAllAppsButton != null) { - mAllAppsButton.post(() -> { - mAllAppsButton.post(() -> { - mAllAppsTouchTriggered = false; - }); - }); + private float getTaskBarIconsEndForBubbleBarLocation(BubbleBarLocation location) { + DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); + boolean navbarOnRight = location.isOnLeft(isLayoutRtl()); + int navSpaceNeeded = deviceProfile.getHotseatProfile().getBarEndOffset(); + if (navbarOnRight) { + return getWidth() - navSpaceNeeded; + } else { + return navSpaceNeeded + getIconLayoutWidth(); } } - private void onAllAppsButtonClick(View view) { - if (!mAllAppsTouchTriggered) { - mControllerCallbacks.triggerAllAppsButtonClick(view); - } + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + return new TaskbarLayoutParams(lp); } - // Handle long click from Switch Access and Voice Access - private boolean onAllAppsButtonLongClick(View view) { - if (!MAIN_EXECUTOR.getHandler().hasCallbacks(mAllAppsTouchRunnable) - && !mAllAppsTouchTriggered) { - mControllerCallbacks.triggerAllAppsButtonLongClick(); + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new TaskbarLayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof TaskbarLayoutParams; + } + + public static class TaskbarLayoutParams extends FrameLayout.LayoutParams { + + @Nullable public CellInfo bindInfo; + + public TaskbarLayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TaskbarLayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public TaskbarLayoutParams(int width, int height) { + super(width, height); } - return true; } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java index 3c646cb7fa..59707f50d8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java @@ -16,15 +16,28 @@ package com.android.launcher3.taskbar; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION; + +import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP; +import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW; +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.GestureDetector; +import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.jank.Cuj; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Callbacks for {@link TaskbarView} to interact with its controller. @@ -34,12 +47,14 @@ public class TaskbarViewCallbacks { private final TaskbarActivityContext mActivity; private final TaskbarControllers mControllers; private final TaskbarView mTaskbarView; + private final GestureDetector mGestureDetector; public TaskbarViewCallbacks(TaskbarActivityContext activity, TaskbarControllers controllers, TaskbarView taskbarView) { mActivity = activity; mControllers = controllers; mTaskbarView = taskbarView; + mGestureDetector = new GestureDetector(activity, new TaskbarViewGestureListener()); } public View.OnClickListener getIconOnClickListener() { @@ -47,25 +62,40 @@ public class TaskbarViewCallbacks { } /** Trigger All Apps button click action. */ - protected void triggerAllAppsButtonClick(View v) { + public void triggerAllAppsButtonClick(View v) { InteractionJankMonitorWrapper.begin(v, Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS, /* tag= */ "TASKBAR_BUTTON"); mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP); - mControllers.taskbarAllAppsController.toggle(); + if (mActivity.showLockedTaskbarOnHome() + || mActivity.showDesktopTaskbarForFreeformDisplay()) { + // If the taskbar can be shown on the home screen, use mAllAppsToggler to toggle all + // apps, which will toggle the launcher activity all apps when on home screen. + // TODO(b/395913143): Reconsider this if a gap in taskbar all apps functionality that + // prevents users to drag items to workspace is addressed. + mControllers.uiController.toggleAllApps(false); + } else { + mControllers.taskbarAllAppsController.toggle(); + } } /** Trigger All Apps button long click action. */ - protected void triggerAllAppsButtonLongClick() { + public void triggerAllAppsButtonLongClick() { mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS); } - public boolean isAllAppsButtonHapticFeedbackEnabled() { + /** @return true if haptic feedback should occur when long pressing the all apps button. */ + public boolean isAllAppsButtonHapticFeedbackEnabled(Context context) { return false; } + @SuppressLint("ClickableViewAccessibility") + public View.OnTouchListener getTaskbarTouchListener() { + return (view, event) -> mGestureDetector.onTouchEvent(event); + } + public View.OnLongClickListener getTaskbarDividerLongClickListener() { return v -> { - mControllers.taskbarPinningController.showPinningView(v); + mControllers.taskbarPinningController.showPinningView(v, getDividerCenterX()); return true; }; } @@ -73,8 +103,9 @@ public class TaskbarViewCallbacks { public View.OnTouchListener getTaskbarDividerRightClickListener() { return (v, event) -> { if (event.isFromSource(InputDevice.SOURCE_MOUSE) + && event.getAction() == MotionEvent.ACTION_DOWN && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) { - mControllers.taskbarPinningController.showPinningView(v); + mControllers.taskbarPinningController.showPinningView(v, getDividerCenterX()); return true; } return false; @@ -90,11 +121,18 @@ public class TaskbarViewCallbacks { return new TaskbarHoverToolTipController(mActivity, mTaskbarView, icon); } + /** Callback invoked before Taskbar icons are laid out. */ + void onPreLayoutChildren() { + if (enableTaskbarPinning() && ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue()) { + mControllers.taskbarViewController.updateTaskbarIconTranslationXForPinning(); + } + } + /** * Notifies launcher to update icon alignment. */ public void notifyIconLayoutBoundsChanged() { - mControllers.uiController.onIconLayoutBoundsChanged(); + mControllers.taskbarViewController.notifyIconLayoutBoundsChanged(); } /** @@ -104,4 +142,116 @@ public class TaskbarViewCallbacks { mControllers.taskbarScrimViewController.onTaskbarVisibilityChanged( mTaskbarView.getVisibility()); } + + /** + * Get current location of bubble bar, if it is visible. + * Returns {@code null} if bubble bar is not shown. + */ + @Nullable + public BubbleBarLocation getBubbleBarLocationIfVisible() { + BubbleBarViewController bubbleBarViewController = + mControllers.bubbleControllers.map(c -> c.bubbleBarViewController).orElse(null); + if (bubbleBarViewController != null && bubbleBarViewController.isBubbleBarVisible()) { + return bubbleBarViewController.getBubbleBarLocation(); + } + return null; + } + + /** + * Get the max bubble bar collapsed width for the current bubble bar visibility state. Used to + * reserve space for the bubble bar when transitioning taskbar view into overflow. + */ + public float getBubbleBarMaxCollapsedWidthIfVisible() { + return mControllers.bubbleControllers + .filter(c -> !c.bubbleBarViewController.isHiddenForNoBubbles()) + .map(c -> c.bubbleBarViewController.getCollapsedWidthWithMaxVisibleBubbles()) + .orElse(0f); + } + + /** Returns true if bubble bar controllers are present. */ + public boolean isBubbleBarEnabled() { + return mControllers.bubbleControllers.isPresent(); + } + + /** Returns on click listener for the taskbar overflow view. */ + public View.OnClickListener getOverflowOnClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + toggleKeyboardQuickSwitchView(); + } + }; + } + + /** Returns on long click listener for the taskbar overflow view. */ + public View.OnLongClickListener getOverflowOnLongClickListener() { + return new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + toggleKeyboardQuickSwitchView(); + return true; + } + }; + } + + private void toggleKeyboardQuickSwitchView() { + if (mTaskbarView.getTaskbarOverflowView() != null) { + mTaskbarView.getTaskbarOverflowView().setIsActive( + !mTaskbarView.getTaskbarOverflowView().getIsActive()); + mControllers.taskbarAutohideSuspendController + .updateFlag(FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW, + mTaskbarView.getTaskbarOverflowView().getIsActive()); + } + mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar( + mControllers.taskbarViewController.getShownTaskIds(), + this::onKeyboardQuickSwitchViewClosed); + } + + private void onKeyboardQuickSwitchViewClosed() { + if (mTaskbarView.getTaskbarOverflowView() != null) { + mTaskbarView.getTaskbarOverflowView().setIsActive(false); + } + mControllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW, false); + } + + private float getDividerCenterX() { + View divider = mTaskbarView.getTaskbarDividerViewContainer(); + if (divider == null) { + return 0.0f; + } + return divider.getX() + (float) divider.getWidth() / 2; + } + + private class TaskbarViewGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent event) { + if (event.isFromSource(InputDevice.SOURCE_MOUSE) + && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) { + maybeShowPinningView(event); + } + return true; + } + + @Override + public boolean onSingleTapUp(@NonNull MotionEvent event) { + return true; + } + + @Override + public void onLongPress(@NonNull MotionEvent event) { + if (maybeShowPinningView(event)) { + mTaskbarView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + } + + /** Returns true if the taskbar pinning popup view was shown for {@code event}. */ + private boolean maybeShowPinningView(@NonNull MotionEvent event) { + if (!mActivity.isPinnedTaskbar() || mTaskbarView.isEventOverAnyItem(event)) { + return false; + } + mControllers.taskbarPinningController.showPinningView(mTaskbarView, event.getRawX()); + return true; + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt index ba0f5a01b0..bca51e7e82 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt @@ -16,28 +16,55 @@ package com.android.launcher3.taskbar +import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_META import android.content.Context -import com.android.launcher3.R -import com.android.launcher3.util.ResourceBasedOverride -import com.android.launcher3.util.ResourceBasedOverride.Overrides +import com.android.launcher3.dagger.LauncherComponentProvider +import com.android.launcher3.logging.StatsLogManager +import com.android.quickstep.TopTaskTracker +import com.android.quickstep.util.ContextualSearchInvoker +import javax.inject.Inject /** Creates [TaskbarViewCallbacks] instances. */ -open class TaskbarViewCallbacksFactory : ResourceBasedOverride { +open class TaskbarViewCallbacksFactory @Inject constructor() { open fun create( activity: TaskbarActivityContext, controllers: TaskbarControllers, taskbarView: TaskbarView, - ): TaskbarViewCallbacks = TaskbarViewCallbacks(activity, controllers, taskbarView) + ): TaskbarViewCallbacks { + return object : TaskbarViewCallbacks(activity, controllers, taskbarView) { + override fun triggerAllAppsButtonLongClick() { + super.triggerAllAppsButtonLongClick() + + val contextualSearchInvoked = + ContextualSearchInvoker(activity).show(ENTRYPOINT_LONG_PRESS_META) + if (contextualSearchInvoked) { + val runningPackage = + TopTaskTracker.INSTANCE[activity].getCachedTopTask( + /* filterOnlyVisibleRecents */ true, + activity.displayId, + ) + .getPackageName() + activity.statsLogManager + .logger() + .withPackageName(runningPackage) + .log(StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META) + } + } + + override fun isAllAppsButtonHapticFeedbackEnabled(context: Context): Boolean { + return longPressAllAppsToStartContextualSearch(context) + } + } + } + + open fun longPressAllAppsToStartContextualSearch(context: Context): Boolean = + ContextualSearchInvoker(context).runContextualSearchInvocationChecksAndLogFailures() companion object { @JvmStatic fun newInstance(context: Context): TaskbarViewCallbacksFactory { - return Overrides.getObject( - TaskbarViewCallbacksFactory::class.java, - context, - R.string.taskbar_view_callbacks_factory_class, - ) + return LauncherComponentProvider.get(context).getTaskbarViewCallbacksFactory() } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 55745b557a..afcbc852c3 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -15,6 +15,15 @@ */ package com.android.launcher3.taskbar; +import static android.animation.LayoutTransition.APPEARING; +import static android.animation.LayoutTransition.CHANGE_APPEARING; +import static android.animation.LayoutTransition.CHANGE_DISAPPEARING; +import static android.animation.LayoutTransition.DISAPPEARING; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_OVERFLOW; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION; + +import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.app.animation.Interpolators.FINAL_FRAME; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; @@ -29,23 +38,32 @@ import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UN import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT; +import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS; +import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; +import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_BUBBLE_BAR_ANIM; +import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_NAV_BAR_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_PINNING_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_REVEAL_ANIM; import android.animation.Animator; import android.animation.AnimatorSet; +import android.animation.LayoutTransition; +import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.graphics.Rect; +import android.util.FloatProperty; import android.util.Log; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.animation.Interpolator; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.view.OneShotPreDrawListener; import com.android.app.animation.Interpolators; @@ -62,28 +80,44 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.RevealOutlineAnimation; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.util.DisplayController; +import com.android.launcher3.model.data.TaskItemInfo; +import com.android.launcher3.taskbar.bubbles.BubbleBarController; +import com.android.launcher3.taskbar.bubbles.BubbleControllers; +import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer; +import com.android.launcher3.taskbar.customization.TaskbarDividerContainer; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.MultiPropertyFactory; +import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.SandboxContext; import com.android.launcher3.views.IconButtonView; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SingleTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; +import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.function.Predicate; /** * Handles properties/data collection, then passes the results to TaskbarView to render. */ -public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController { +public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController, + BubbleBarController.BubbleBarLocationListener { private static final String TAG = "TaskbarViewController"; private static final Runnable NO_OP = () -> { }; + public static long TRANSLATION_X_FOR_BUBBLEBAR_ANIM_DURATION_MS = 250; + public static final int ALPHA_INDEX_HOME = 0; public static final int ALPHA_INDEX_KEYGUARD = 1; public static final int ALPHA_INDEX_STASH = 2; @@ -91,13 +125,28 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4; public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 5; public static final int ALPHA_INDEX_SMALL_SCREEN = 6; - private static final int NUM_ALPHA_CHANNELS = 7; + public static final int ALPHA_INDEX_BUBBLE_BAR = 7; + public static final int ALPHA_INDEX_RECREATE = 8; + + private static final int NUM_ALPHA_CHANNELS = 9; + + /** Only used for animation purposes, to position the divider between two item indices. */ + public static final float DIVIDER_VIEW_POSITION_OFFSET = 0.5f; + + /** Used if an unexpected edge case is hit in {@link #getPositionInHotseat}. */ + private static final float ERROR_POSITION_IN_HOTSEAT_NOT_FOUND = -100; + + private static final int TRANSITION_DELAY = 50; + static final int TRANSITION_DEFAULT_DURATION = 500; + private static final int TRANSITION_FADE_IN_DURATION = 167; + private static final int TRANSITION_FADE_OUT_DURATION = 83; private final TaskbarActivityContext mActivity; + private @Nullable TaskbarDragLayerController mDragLayerController; private final TaskbarView mTaskbarView; private final MultiValueAlpha mTaskbarIconAlpha; private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale); - private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat( + public final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat( this::updateTranslationY); private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat( this::updateTranslationY); @@ -106,15 +155,29 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar this::updateTaskbarIconsScale); private final AnimatedFloat mTaskbarIconTranslationXForPinning = new AnimatedFloat( - this::updateTaskbarIconTranslationXForPinning); + () -> updateTaskbarIconTranslationXForPinning()); + + private final AnimatedFloat mIconsTranslationXForNavbar = new AnimatedFloat( + this::updateTranslationXForNavBar); + + private final AnimatedFloat mTranslationXForBubbleBar = new AnimatedFloat( + this::updateTranslationXForBubbleBar); + + private final TransitionEndBoundsChangedNotifier mTransitionEndBoundsChangedNotifier = + new TransitionEndBoundsChangedNotifier(); + + @Nullable + private Animator mTaskbarShiftXAnim; + @Nullable + private BubbleBarLocation mCurrentBubbleBarLocation; + @Nullable + private BubbleControllers mBubbleControllers = null; + @Nullable + private ObjectAnimator mTranslationXAnimation; private final AnimatedFloat mTaskbarIconTranslationYForPinning = new AnimatedFloat( this::updateTranslationY); - private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener = - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) - -> updateTaskbarIconTranslationXForPinning(); - private AnimatedFloat mTaskbarNavButtonTranslationY; private AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay; @@ -129,12 +192,24 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar // Initialized in init. private TaskbarControllers mControllers; + private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (!ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue()) { + // update shiftX is handled with the animation at the end of the method + updateTaskbarIconTranslationXForPinning(/* updateShiftXForBubbleBar = */ false); + } + if (mBubbleControllers == null) return; + mControllers.navbarButtonsViewController.onLayoutsUpdated(); + adjustTaskbarXForBubbleBar(); + }; + // Animation to align icons with Launcher, created lazily. This allows the controller to be // active only during the animation and does not need to worry about layout changes. private AnimatorPlaybackController mIconAlignControllerLazy = null; private Runnable mOnControllerPreCreateCallback = NO_OP; // Stored here as signals to determine if the mIconAlignController needs to be recreated. + private boolean mIsIconAlignedWithHotseat; private boolean mIsHotseatIconOnTopWhenAligned; private boolean mIsStashed; @@ -151,66 +226,169 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar private final float mTaskbarLeftRightMargin; + private final TaskbarRunningAppStateAnimationController mRunningStateController; + public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) { mActivity = activity; mTransientTaskbarDp = mActivity.getTransientTaskbarDeviceProfile(); mPersistentTaskbarDp = mActivity.getPersistentTaskbarDeviceProfile(); - mTransientIconSize = mTransientTaskbarDp.taskbarIconSize; - mPersistentIconSize = mPersistentTaskbarDp.taskbarIconSize; + mTransientIconSize = mTransientTaskbarDp.getTaskbarProfile().getIconSize(); + mPersistentIconSize = mPersistentTaskbarDp.getTaskbarProfile().getIconSize(); mTaskbarView = taskbarView; mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, NUM_ALPHA_CHANNELS); mTaskbarIconAlpha.setUpdateVisibility(true); mModelCallbacks = TaskbarModelCallbacksFactory.newInstance(mActivity) .create(mActivity, mTaskbarView); - mTaskbarBottomMargin = activity.getDeviceProfile().taskbarBottomMargin; + mTaskbarBottomMargin = activity.getDeviceProfile().getTaskbarProfile().getBottomMargin(); mStashedHandleHeight = activity.getResources() .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_height); mIsRtl = Utilities.isRtl(mTaskbarView.getResources()); mTaskbarLeftRightMargin = mActivity.getResources().getDimensionPixelSize( R.dimen.transient_taskbar_padding); - + mRunningStateController = new TaskbarRunningAppStateAnimationController(mActivity); } - public void init(TaskbarControllers controllers) { + /** + * Init of taskbar view controller. + */ + public void init(TaskbarControllers controllers, AnimatorSet startAnimation) { mControllers = controllers; + controllers.bubbleControllers.ifPresent(bc -> mBubbleControllers = bc); + + if (startAnimation != null) { + MultiPropertyFactory.MultiProperty multiProperty = + mTaskbarIconAlpha.get(ALPHA_INDEX_RECREATE); + multiProperty.setValue(0f); + Animator animator = multiProperty.animateToValue(1f); + animator.setInterpolator(EMPHASIZED); + startAnimation.play(animator); + } + mTaskbarView.init(TaskbarViewCallbacksFactory.newInstance(mActivity).create( mActivity, mControllers, mTaskbarView)); + // Pinning popup feature availability depends on taskbar controllers, wait for the + // controllers state initialization before evaluating the feature. + mControllers.runAfterInit(mTaskbarView::updatePinningPopupEventHandlers); mTaskbarView.getLayoutParams().height = mActivity.isPhoneMode() ? mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_phone_size) - : mActivity.getDeviceProfile().taskbarHeight; + : mActivity.getDeviceProfile().getTaskbarProfile().getHeight(); mTaskbarIconScaleForStash.updateValue(1f); - float pinningValue = DisplayController.isTransientTaskbar(mActivity) - ? PINNING_TRANSIENT - : PINNING_PERSISTENT; + float pinningValue = + mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT; mTaskbarIconScaleForPinning.updateValue(pinningValue); mTaskbarIconTranslationYForPinning.updateValue(pinningValue); mTaskbarIconTranslationXForPinning.updateValue(pinningValue); mModelCallbacks.init(controllers); - if (mActivity.isUserSetupComplete()) { + if (mActivity.isUserSetupComplete() + && !(mActivity.getApplicationContext() instanceof SandboxContext)) { // Only load the callbacks if user setup is completed - LauncherAppState.getInstance(mActivity).getModel().addCallbacksAndLoad(mModelCallbacks); + controllers.runAfterInit(() -> LauncherAppState.getInstance(mActivity).getModel() + .addCallbacksAndLoad(mModelCallbacks)); } mTaskbarNavButtonTranslationY = controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY(); mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController .getTaskbarNavButtonTranslationYForInAppDisplay(); - + mDragLayerController = controllers.taskbarDragLayerController; mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener); if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { // This gets modified in NavbarButtonsViewController, but the initial value it reads // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here - mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN) - .animateToValue(mActivity.isPhoneButtonNavMode() ? 0 : 1).start(); + mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN).setValue( + mActivity.isPhoneMode() ? 0 : 1); } if (enableTaskbarPinning()) { mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener); } } + /** + * Called whenever a new ui controller is set. + */ + public void onUiControllerChanged() { + // Pinning availability may depend on UI state when home has "locked" pinned taskbar. + mTaskbarView.updatePinningPopupEventHandlers(); + } + + /** Adjusts start aligned taskbar layout accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationUpdated(BubbleBarLocation location) { + updateCurrentBubbleBarLocation(location); + if (mActivity.isTransientTaskbar()) { + translateTaskbarXForBubbleBar(/* animate= */ false); + } else if (mActivity.shouldStartAlignTaskbar()) { + cancelTaskbarShiftAnimation(); + // reset translation x, taskbar will position icons with the updated location + mIconsTranslationXForNavbar.updateValue(0); + mTaskbarView.onBubbleBarLocationUpdated(location); + } + } + + /** Animates start aligned taskbar accordingly to the bubble bar position. */ + @Override + public void onBubbleBarLocationAnimated(BubbleBarLocation location) { + boolean locationUpdated = updateCurrentBubbleBarLocation(location); + if (mActivity.isTransientTaskbar()) { + translateTaskbarXForBubbleBar(/* animate= */ true); + } else if (locationUpdated && mActivity.shouldStartAlignTaskbar()) { + cancelTaskbarShiftAnimation(); + float translationX = mTaskbarView.getTranslationXForBubbleBarPosition(location); + mTaskbarShiftXAnim = createTaskbarIconsShiftAnimator(translationX); + mTaskbarShiftXAnim.start(); + } + } + + private void translateTaskbarXForBubbleBar(boolean animate) { + cancelCurrentTranslationXAnimation(); + if (!mActivity.isTransientTaskbar()) return; + int shiftX = getTransientTaskbarShiftXForBubbleBar(); + if (animate) { + mTranslationXAnimation = mTranslationXForBubbleBar.animateToValue(shiftX); + mTranslationXAnimation.setInterpolator(EMPHASIZED); + mTranslationXAnimation.setDuration(TRANSLATION_X_FOR_BUBBLEBAR_ANIM_DURATION_MS); + mTranslationXAnimation.start(); + } else { + mTranslationXForBubbleBar.updateValue(shiftX); + } + } + + private void cancelCurrentTranslationXAnimation() { + if (mTranslationXAnimation != null) { + if (mTranslationXAnimation.isRunning()) { + mTranslationXAnimation.cancel(); + } + mTranslationXAnimation = null; + } + } + + private int getTransientTaskbarShiftXForBubbleBar() { + if (mBubbleControllers == null || !mActivity.isTransientTaskbar()) { + return 0; + } + return mBubbleControllers.bubbleBarViewController + .getTransientTaskbarTranslationXForBubbleBar(mCurrentBubbleBarLocation); + } + + /** Updates the mCurrentBubbleBarLocation, returns {@code} true if location is updated. */ + private boolean updateCurrentBubbleBarLocation(BubbleBarLocation location) { + if (mCurrentBubbleBarLocation == location || location == null) { + return false; + } else { + mCurrentBubbleBarLocation = location; + return true; + } + } + + private void cancelTaskbarShiftAnimation() { + if (mTaskbarShiftXAnim != null) { + mTaskbarShiftXAnim.cancel(); + } + } + /** * Announcement for Accessibility when Taskbar stashes/unstashes. */ @@ -218,13 +396,29 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar mTaskbarView.announceAccessibilityChanges(); } + /** + * Called with destroying Taskbar with animation. + */ + public void onDestroyAnimation(AnimatorSet animatorSet) { + animatorSet.play( + mTaskbarIconAlpha.get(TaskbarViewController.ALPHA_INDEX_RECREATE).animateToValue( + 0f)); + } + public void onDestroy() { if (enableTaskbarPinning()) { mTaskbarView.removeOnLayoutChangeListener(mTaskbarViewLayoutChangeListener); } LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks); mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener); - mModelCallbacks.unregisterListeners(); + mRunningStateController.onDestroy(); + } + + /** + * Gets the taskbar {@link View.Visibility visibility}. + */ + public int getTaskbarVisibility() { + return mTaskbarView.getVisibility(); } public boolean areIconsVisible() { @@ -235,12 +429,19 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar return mTaskbarIconAlpha; } + /** Creates a ModelWriter for updating model properties */ + public ModelWriter getModelWriter() { + return LauncherAppState.getInstance(mActivity).getModel() + .getWriter(false, mActivity.getCellPosMapper(), mModelCallbacks); + } + /** * Should be called when the recents button is disabled, so we can hide Taskbar icons as well. */ public void setRecentsButtonDisabled(boolean isDisabled) { // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha. - mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1); + mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).animateToValue(isDisabled ? 0 : 1) + .start(); } /** @@ -259,21 +460,25 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar OneShotPreDrawListener.add(mTaskbarView, listener); } - public Rect getIconLayoutBounds() { - return mTaskbarView.getIconLayoutBounds(); + @VisibleForTesting + int getMaxNumIconViews() { + return mTaskbarView.getMaxNumIconViews(); } - public int getIconLayoutWidth() { - return mTaskbarView.getIconLayoutWidth(); + public Rect getTransientTaskbarIconLayoutBounds() { + return mTaskbarView.getTransientTaskbarIconLayoutBounds(); + } + + public Rect getTransientTaskbarIconLayoutBoundsInParent() { + return mTaskbarView.getTransientTaskbarIconLayoutBoundsInParent(); } public View[] getIconViews() { return mTaskbarView.getIconViews(); } - @Nullable public View getAllAppsButtonView() { - return mTaskbarView.getAllAppsButtonView(); + return mTaskbarView.getAllAppsButtonContainer(); } public AnimatedFloat getTaskbarIconScaleForStash() { @@ -313,7 +518,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar View[] iconViews = mTaskbarView.getIconViews(); float finalScale; - if (mControllers.getSharedState().startTaskbarVariantIsTransient) { + TaskbarSharedState sharedState = mControllers.getSharedState(); + if (sharedState != null && sharedState.startTaskbarVariantIsTransient) { finalScale = mapRange(scale, 1f, ((float) mPersistentIconSize / mTransientIconSize)); } else { finalScale = mapRange(scale, ((float) mTransientIconSize / mPersistentIconSize), 1f); @@ -336,16 +542,39 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } } - private void updateTaskbarIconTranslationXForPinning() { + void updateTaskbarIconTranslationXForPinning() { + updateTaskbarIconTranslationXForPinning(/* updateShiftXForBubbleBar = */ true); + } + + void updateTaskbarIconTranslationXForPinning(boolean updateShiftXForBubbleBar) { View[] iconViews = mTaskbarView.getIconViews(); float scale = mTaskbarIconTranslationXForPinning.value; float transientTaskbarAllAppsOffset = mActivity.getResources().getDimension( - mTaskbarView.getAllAppsButtonTranslationXOffset(true)); + mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(true)); float persistentTaskbarAllAppsOffset = mActivity.getResources().getDimension( - mTaskbarView.getAllAppsButtonTranslationXOffset(false)); - + mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(false)); + if (mBubbleControllers != null && updateShiftXForBubbleBar) { + cancelCurrentTranslationXAnimation(); + int translationXForTransientTaskbar = mBubbleControllers.bubbleBarViewController + .getTransientTaskbarTranslationXForBubbleBar(mCurrentBubbleBarLocation); + float currentTranslationXForTransientTaskbar = mapRange(scale, + translationXForTransientTaskbar, 0); + mTranslationXForBubbleBar.updateValue(currentTranslationXForTransientTaskbar); + } float allAppIconTranslateRange = mapRange(scale, transientTaskbarAllAppsOffset, persistentTaskbarAllAppsOffset); + // Task icons are laid out so the taskbar content is centered. The taskbar width (used for + // centering taskbar icons) depends on the all apps button X translation, and is different + // for persistent and transient taskbar. If the offset used for current taskbar layout is + // different than the offset used in final taskbar state, the icons may jump when the + // animation completes, and the taskbar is replaced. Adjust item transform to account for + // this mismatch. + float sizeDiffTranslationRange = + mapRange(scale, + (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout() + - transientTaskbarAllAppsOffset) / 2, + (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout() + - persistentTaskbarAllAppsOffset) / 2); // no x translation required when all apps button is the only icon in taskbar. if (iconViews.length <= 1) { @@ -354,36 +583,30 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar if (mIsRtl) { allAppIconTranslateRange *= -1; + sizeDiffTranslationRange *= -1; } - if (mActivity.isThreeButtonNav()) { - ((IconButtonView) mTaskbarView.getAllAppsButtonView()) + if (!mTaskbarView.canTransitionToTransientTaskbar()) { + mTaskbarView.getAllAppsButtonContainer() .setTranslationXForTaskbarAllAppsIcon(allAppIconTranslateRange); return; } - float taskbarCenterX = - mTaskbarView.getLeft() + (mTaskbarView.getRight() - mTaskbarView.getLeft()) / 2.0f; - float finalMarginScale = mapRange(scale, 0f, mTransientIconSize - mPersistentIconSize); - float halfIconCount = iconViews.length / 2.0f; + // The index of the "middle" icon which will be used as a index from which the icon margins + // will be scaled. If number of icons is even, using the middle point between indices of two + // central icons. + float middleIndex = (iconViews.length - 1) / 2.0f; for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) { View iconView = iconViews[iconIndex]; MultiTranslateDelegate translateDelegate = ((Reorderable) iconView).getTranslateDelegate(); - float iconCenterX = - iconView.getLeft() + (iconView.getRight() - iconView.getLeft()) / 2.0f; - if (iconCenterX <= taskbarCenterX) { - translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue( - finalMarginScale * (halfIconCount - iconIndex)); - } else { - translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue( - -finalMarginScale * (iconIndex - halfIconCount)); - } + translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue( + finalMarginScale * (middleIndex - iconIndex) + sizeDiffTranslationRange); - if (iconView.equals(mTaskbarView.getAllAppsButtonView())) { - ((IconButtonView) iconView).setTranslationXForTaskbarAllAppsIcon( + if (iconView.equals(mTaskbarView.getAllAppsButtonContainer())) { + mTaskbarView.getAllAppsButtonContainer().setTranslationXForTaskbarAllAppsIcon( allAppIconTranslateRange); } } @@ -393,18 +616,14 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar * Calculates visual taskbar view width. */ public float getCurrentVisualTaskbarWidth() { - if (mTaskbarView.getIconViews().length == 0) { + View[] iconViews = mTaskbarView.getIconViews(); + if (iconViews.length == 0) { return 0; } - View[] iconViews = mTaskbarView.getIconViews(); + float left = iconViews[0].getX(); - int leftIndex = mActivity.getDeviceProfile().isQsbInline && !mIsRtl ? 1 : 0; - int rightIndex = mActivity.getDeviceProfile().isQsbInline && mIsRtl - ? iconViews.length - 2 - : iconViews.length - 1; - - float left = iconViews[leftIndex].getX(); + int rightIndex = iconViews.length - 1; float right = iconViews[rightIndex].getRight() + iconViews[rightIndex].getTranslationX(); return right - left + (2 * mTaskbarLeftRightMargin); @@ -434,6 +653,27 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar + mTaskbarIconTranslationYForSpringOnStash); } + private void updateTranslationXForNavBar() { + updateIconViewsTranslationX(INDEX_NAV_BAR_ANIM, mIconsTranslationXForNavbar.value); + } + + private void updateTranslationXForBubbleBar() { + float translationX = mTranslationXForBubbleBar.value; + updateIconViewsTranslationX(INDEX_BUBBLE_BAR_ANIM, translationX); + if (mDragLayerController != null) { + mDragLayerController.setTranslationXForBubbleBar(translationX); + } + } + + private void updateIconViewsTranslationX(int translationXChannel, float translationX) { + View[] iconViews = mTaskbarView.getIconViews(); + for (View iconView : iconViews) { + MultiTranslateDelegate translateDelegate = + ((Reorderable) iconView).getTranslateDelegate(); + translateDelegate.getTranslationX(translationXChannel).setValue(translationX); + } + } + /** * Computes translation y for taskbar pinning. */ @@ -448,17 +688,19 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar // finally placing the icon in the middle of new taskbar background height. if (mControllers.getSharedState().startTaskbarVariantIsTransient) { float transY = - mTransientTaskbarDp.taskbarBottomMargin + (mTransientTaskbarDp.taskbarHeight - - mTaskbarView.getIconLayoutBounds().bottom) - - (mPersistentTaskbarDp.taskbarHeight - - mTransientTaskbarDp.taskbarIconSize) / 2f; + mTransientTaskbarDp.getTaskbarProfile().getBottomMargin() + ( + mTransientTaskbarDp.getTaskbarProfile().getHeight() + - mTaskbarView.getTransientTaskbarIconLayoutBounds().bottom) + - (mPersistentTaskbarDp.getTaskbarProfile().getHeight() + - mTransientTaskbarDp.getTaskbarProfile().getIconSize()) / 2f; taskbarIconTranslationYForPinningValue = mapRange(scale, 0f, transY); } else { float transY = - -mTransientTaskbarDp.taskbarBottomMargin + (mPersistentTaskbarDp.taskbarHeight - - mTaskbarView.getIconLayoutBounds().bottom) - - (mTransientTaskbarDp.taskbarHeight - - mTransientTaskbarDp.taskbarIconSize) / 2f; + -mTransientTaskbarDp.getTaskbarProfile().getBottomMargin() + ( + mPersistentTaskbarDp.getTaskbarProfile().getHeight() + - mTaskbarView.getTransientTaskbarIconLayoutBounds().bottom) + - (mTransientTaskbarDp.getTaskbarProfile().getHeight() + - mTransientTaskbarDp.getTaskbarProfile().getIconSize()) / 2f; taskbarIconTranslationYForPinningValue = mapRange(scale, transY, 0f); } return taskbarIconTranslationYForPinningValue; @@ -511,30 +753,72 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } public View getTaskbarDividerView() { - return mTaskbarView.getTaskbarDividerView(); + return mTaskbarView.getTaskbarDividerViewContainer(); } - /** Updates which icons are marked as running given the Set of currently running packages. */ - public void updateIconViewsRunningStates(Set runningPackages, - Set minimizedPackages) { + /** + * Updates which icons are marked as running or minimized given the Sets of currently running + * and minimized tasks. + */ + public void updateIconViewsRunningStates() { for (View iconView : getIconViews()) { if (iconView instanceof BubbleTextView btv) { - btv.updateRunningState( - getRunningAppState(btv.getTargetPackageName(), runningPackages, - minimizedPackages)); + updateRunningState(btv); + if (shouldUpdateIconContentDescription(btv)) { + btv.setContentDescription( + btv.getContentDescription() + " " + btv.getIconStateDescription()); + } } } } - private BubbleTextView.RunningAppState getRunningAppState( - String packageName, - Set runningPackages, - Set minimizedPackages) { - if (minimizedPackages.contains(packageName)) { - return BubbleTextView.RunningAppState.MINIMIZED; + private boolean shouldUpdateIconContentDescription(BubbleTextView btv) { + boolean isInDesktopMode = + mControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar( + DEFAULT_DISPLAY); + boolean isAllAppsButton = btv instanceof TaskbarAllAppsButtonContainer; + boolean isDividerButton = btv instanceof TaskbarDividerContainer; + return isInDesktopMode && !isAllAppsButton && !isDividerButton; + } + + /** + * @return A set of Task ids shown in the taskbar - includes task ID for running tasks of pinned + * apps, and standalone running tasks. + */ + protected Set getShownTaskIds() { + if (!ENABLE_TASKBAR_OVERFLOW.isTrue()) { + return Collections.emptySet(); } - if (runningPackages.contains(packageName)) { - return BubbleTextView.RunningAppState.RUNNING; + + Set shownTasks = new HashSet<>(); + for (View iconView : getIconViews()) { + if (iconView instanceof BubbleTextView btv) { + if (btv.getTag() instanceof TaskItemInfo itemInfo) { + shownTasks.add(itemInfo.getTaskId()); + } else if (btv.getTag() instanceof SingleTask task) { + shownTasks.add(task.getTask().getKey().id); + } + } + } + return shownTasks; + } + + private void updateRunningState(BubbleTextView btv) { + mRunningStateController.updateRunningState( + btv, + getRunningAppState(btv), + /* animate = */ mTaskbarView.getLayoutTransition() != null); + } + + private BubbleTextView.RunningAppState getRunningAppState(BubbleTextView btv) { + Object tag = btv.getTag(); + if (tag instanceof TaskItemInfo itemInfo) { + return mControllers.taskbarRecentAppsController.getRunningAppState( + itemInfo.getTaskId()); + } + if (tag instanceof SingleTask singleTask) { + return mControllers.taskbarRecentAppsController.getRunningAppState( + singleTask.getTask().key.id); } return BubbleTextView.RunningAppState.NOT_RUNNING; } @@ -624,6 +908,16 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar as.play(reveal); } + void notifyIconLayoutBoundsChanged() { + final LayoutTransition layoutTransition = mTaskbarView.getLayoutTransition(); + if (layoutTransition != null && layoutTransition.isRunning()) { + // Defers notify until after transitions finish. + mTransitionEndBoundsChangedNotifier.mIsCanceled = false; + } else { + mControllers.uiController.onIconLayoutBoundsChanged(); + } + } + /** * Sets the Taskbar icon alignment relative to Launcher hotseat icons * @param alignmentRatio [0, 1] @@ -635,16 +929,24 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar mIconAlignControllerLazy = null; return; } - boolean isHotseatIconOnTopWhenAligned = mControllers.uiController.isHotseatIconOnTopWhenAligned(); + boolean isIconAlignedWithHotseat = mControllers.uiController.isIconAlignedWithHotseat(); boolean isStashed = mControllers.taskbarStashController.isStashed(); - // Re-create animation when mIsHotseatIconOnTopWhenAligned or mIsStashed changes. + // Re-create animation when any of these values change. if (mIconAlignControllerLazy == null || mIsHotseatIconOnTopWhenAligned != isHotseatIconOnTopWhenAligned + || mIsIconAlignedWithHotseat != isIconAlignedWithHotseat || mIsStashed != isStashed) { mIsHotseatIconOnTopWhenAligned = isHotseatIconOnTopWhenAligned; + mIsIconAlignedWithHotseat = isIconAlignedWithHotseat; mIsStashed = isStashed; + + final LayoutTransition layoutTransition = mTaskbarView.getLayoutTransition(); + if (layoutTransition != null && layoutTransition.isRunning()) { + mTransitionEndBoundsChangedNotifier.mIsCanceled = true; + layoutTransition.cancel(); + } mIconAlignControllerLazy = createIconAlignmentController(launcherDp); } mIconAlignControllerLazy.setPlayFraction(alignmentRatio); @@ -654,9 +956,23 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } } - /** Resets the icon alignment controller so that it can be recreated again later. */ - void resetIconAlignmentController() { + /** + * Resets the icon alignment controller so that it can be recreated again later, and updates + * the list of icons shown in the taskbar if the bubble bar visibility changes the taskbar + * overflow state. + */ + void adjustTaskbarForBubbleBar() { mIconAlignControllerLazy = null; + if (mTaskbarView.updateMaxNumIcons()) { + commitRunningAppsToUI(); + } + adjustTaskbarXForBubbleBar(); + } + + private void adjustTaskbarXForBubbleBar() { + if (mBubbleControllers != null && mActivity.isTransientTaskbar()) { + translateTaskbarXForBubbleBar(/* animate= */ true); + } } /** @@ -664,48 +980,88 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar */ private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) { PendingAnimation setter = new PendingAnimation(100); + // icon alignment not needed for pinned taskbar. + if (mActivity.isPinnedTaskbar()) { + return setter.createPlaybackController(); + } mOnControllerPreCreateCallback.run(); DeviceProfile taskbarDp = mActivity.getDeviceProfile(); Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity); - boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity); + boolean isTransientTaskbar = mActivity.isTransientTaskbar(); - float scaleUp = ((float) launcherDp.iconSizePx) / taskbarDp.taskbarIconSize; + float scaleUp = ((float) launcherDp.iconSizePx) + / taskbarDp.getTaskbarProfile().getIconSize(); int borderSpacing = launcherDp.hotseatBorderSpace; int hotseatCellSize = DeviceProfile.calculateCellWidth( - launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right, + launcherDp.getDeviceProperties().getAvailableWidthPx() + - hotseatPadding.left + - hotseatPadding.right, borderSpacing, - launcherDp.numShownHotseatIcons); + launcherDp.numShownHotseatIcons + ); boolean isToHome = mControllers.uiController.isIconAlignedWithHotseat(); + boolean isDeviceLocked = mControllers.taskbarStashController.isDeviceLocked(); // If Hotseat is not the top element, Taskbar should maintain in-app state as it fades out, // or fade in while already in in-app state. Interpolator interpolator = mIsHotseatIconOnTopWhenAligned ? LINEAR : FINAL_FRAME; - int offsetY = launcherDp.getTaskbarOffsetY(); + int offsetY = + isDeviceLocked ? taskbarDp.getTaskbarOffsetY() : launcherDp.getTaskbarOffsetY(); setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, interpolator); setter.setFloat(mTaskbarNavButtonTranslationY, VALUE, -offsetY, interpolator); setter.setFloat(mTaskbarNavButtonTranslationYForInAppDisplay, VALUE, offsetY, interpolator); - + if (mBubbleControllers != null + && mCurrentBubbleBarLocation != null + && mActivity.isTransientTaskbar()) { + int offsetX = mBubbleControllers.bubbleBarViewController + .getTransientTaskbarTranslationXForBubbleBar(mCurrentBubbleBarLocation); + if (offsetX != 0) { + // if taskbar should be adjusted for the bubble bar adjust the taskbar translation + mTranslationXForBubbleBar.updateValue(offsetX); + setter.setFloat(mTranslationXForBubbleBar, VALUE, 0, interpolator); + } + } int collapsedHeight = mActivity.getDefaultTaskbarWindowSize(); - int expandedHeight = Math.max(collapsedHeight, taskbarDp.taskbarHeight + offsetY); + int expandedHeight = Math.max(collapsedHeight, + taskbarDp.getTaskbarProfile().getHeight() + offsetY); setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowSize( anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight)); mTaskbarBottomMargin = isTransientTaskbar - ? mTransientTaskbarDp.taskbarBottomMargin - : mPersistentTaskbarDp.taskbarBottomMargin; + ? mTransientTaskbarDp.getTaskbarProfile().getBottomMargin() + : mPersistentTaskbarDp.getTaskbarProfile().getBottomMargin(); + + int firstRecentTaskIndex = -1; + int hotseatNavBarTranslationX = 0; + if (mCurrentBubbleBarLocation != null) { + boolean isBubblesOnLeft = mCurrentBubbleBarLocation + .isOnLeft(mTaskbarView.isLayoutRtl()); + hotseatNavBarTranslationX = taskbarDp + .getHotseatTranslationXForNavBar(mActivity, isBubblesOnLeft); + } + + int ignoreCount = mTaskbarView.getIgnoreTaskbarIconCount(); for (int i = 0; i < mTaskbarView.getChildCount(); i++) { View child = mTaskbarView.getChildAt(i); - boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonView(); - boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerView(); + boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonContainer(); + boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerViewContainer(); + boolean isTaskbarOverflowView = child == mTaskbarView.getTaskbarOverflowView(); + boolean isRecentTask = child.getTag() instanceof GroupTask; + boolean isRtl = Utilities.isRtl(child.getResources()); + + // TODO(b/343522351): show recents on the home screen. + final boolean isRecentsInHotseat = false; if (!mIsHotseatIconOnTopWhenAligned) { // When going to home, the EMPHASIZED interpolator in TaskbarLauncherStateController // plays iconAlignment to 1 really fast, therefore moving the fading towards the end // to avoid icons disappearing rather than fading out visually. setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0.8f, 1f)); - } else if ((isAllAppsButton && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get()) - || (isTaskbarDividerView && enableTaskbarPinning())) { + } else if ((isAllAppsButton && !FeatureFlags.enableAllAppsButtonInHotseat()) + || (isTaskbarDividerView && enableTaskbarPinning()) + || (isRecentTask && !isRecentsInHotseat) + || isTaskbarOverflowView) { if (!isToHome && mIsHotseatIconOnTopWhenAligned && mIsStashed) { @@ -723,27 +1079,42 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar ? Interpolators.clampToProgress(LINEAR, 0f, 0.17f) : Interpolators.clampToProgress(LINEAR, 0.72f, 0.84f)); } + } else if (((!isRtl && mTaskbarView.getChildCount() - i <= ignoreCount) + || (isRtl && i < ignoreCount)) + && mIsHotseatIconOnTopWhenAligned + && !(child instanceof IconButtonView)) { + setter.addFloat(child, VIEW_ALPHA, 0f, 1f, + isToHome + ? Interpolators.clampToProgress(LINEAR, 0f, 0.35f) + : mActivity.getDeviceProfile().isQsbInline + ? Interpolators.clampToProgress(LINEAR, 0f, 1f) + : Interpolators.clampToProgress(LINEAR, 0.84f, 1f)); + setter.addOnFrameListener(animator -> AlphaUpdateListener.updateVisibility(child)); } - if (child == mTaskbarView.getQsb()) { - boolean isRtl = Utilities.isRtl(child.getResources()); float hotseatIconCenter = isRtl - ? launcherDp.widthPx - hotseatPadding.right + borderSpacing + ? launcherDp.getDeviceProperties().getWidthPx() - hotseatPadding.right + borderSpacing + launcherDp.hotseatQsbWidth / 2f : hotseatPadding.left - borderSpacing - launcherDp.hotseatQsbWidth / 2f; + if (taskbarDp.isQsbInline) { + hotseatIconCenter += hotseatNavBarTranslationX; + } float childCenter = (child.getLeft() + child.getRight()) / 2f; - childCenter += ((Reorderable) child).getTranslateDelegate().getTranslationX( - INDEX_TASKBAR_PINNING_ANIM).getValue(); + if (child instanceof Reorderable reorderableChild) { + childCenter += reorderableChild.getTranslateDelegate().getTranslationX( + INDEX_TASKBAR_PINNING_ANIM).getValue(); + } float halfQsbIconWidthDiff = - (launcherDp.hotseatQsbWidth - taskbarDp.taskbarIconSize) / 2f; - float scale = ((float) taskbarDp.taskbarIconSize) - / launcherDp.hotseatQsbVisualHeight; + (launcherDp.hotseatQsbWidth - taskbarDp.getTaskbarProfile().getIconSize()) + / 2f; + float scale = ((float) taskbarDp.getTaskbarProfile().getIconSize()) + / launcherDp.getHotseatProfile().getQsbVisualHeight(); setter.addFloat(child, SCALE_PROPERTY, scale, 1f, interpolator); float fromX = isRtl ? -halfQsbIconWidthDiff : halfQsbIconWidthDiff; float toX = hotseatIconCenter - childCenter; - if (child instanceof Reorderable) { - MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); + if (child instanceof Reorderable reorderableChild) { + MultiTranslateDelegate mtd = reorderableChild.getTranslateDelegate(); setter.addFloat(mtd.getTranslationX(INDEX_TASKBAR_ALIGNMENT_ANIM), MULTI_PROPERTY_VALUE, fromX, toX, interpolator); @@ -766,30 +1137,24 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar continue; } - float positionInHotseat; - if (isAllAppsButton) { - // Note that there is no All Apps button in the hotseat, - // this position is only used as its convenient for animation purposes. - positionInHotseat = Utilities.isRtl(child.getResources()) - ? taskbarDp.numShownHotseatIcons - : -1; - } else if (isTaskbarDividerView) { - // Note that there is no taskbar divider view in the hotseat, - // this position is only used as its convenient for animation purposes. - positionInHotseat = Utilities.isRtl(child.getResources()) - ? taskbarDp.numShownHotseatIcons - 0.5f - : -0.5f; - } else if (child.getTag() instanceof ItemInfo) { - positionInHotseat = ((ItemInfo) child.getTag()).screenId; - } else { - Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child); - continue; + int recentTaskIndex = -1; + if (isRecentTask) { + if (firstRecentTaskIndex < 0) { + firstRecentTaskIndex = i; + } + recentTaskIndex = i - firstRecentTaskIndex; } + float positionInHotseat = getPositionInHotseat(taskbarDp.numShownHotseatIcons, child, + mIsRtl, isAllAppsButton, isTaskbarDividerView, + mTaskbarView.isDividerForRecents(), recentTaskIndex); + if (positionInHotseat == ERROR_POSITION_IN_HOTSEAT_NOT_FOUND) continue; + - float hotseatAdjustedBorderSpace = - launcherDp.getHotseatAdjustedBorderSpaceForBubbleBar(child.getContext()); float hotseatIconCenter; - if (bubbleBarHasBubbles() && hotseatAdjustedBorderSpace != 0) { + if (launcherDp.shouldAdjustHotseatForBubbleBar(child.getContext(), + bubbleBarHasBubbles())) { + float hotseatAdjustedBorderSpace = + launcherDp.getHotseatAdjustedBorderSpaceForBubbleBar(child.getContext()); hotseatIconCenter = hotseatPadding.left + hotseatCellSize + (hotseatCellSize + hotseatAdjustedBorderSpace) * positionInHotseat + hotseatCellSize / 2f; @@ -798,6 +1163,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar + (hotseatCellSize + borderSpacing) * positionInHotseat + hotseatCellSize / 2f; } + hotseatIconCenter += hotseatNavBarTranslationX; float childCenter = (child.getLeft() + child.getRight()) / 2f; childCenter += ((Reorderable) child).getTranslateDelegate().getTranslationX( INDEX_TASKBAR_PINNING_ANIM).getValue(); @@ -820,9 +1186,61 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar return controller; } + /** + * Returns the index of the given child relative to its position in hotseat. + * Examples: + * -1 is the item before the first hotseat item. + * -0.5 is between those (e.g. for the divider). + * {@link #ERROR_POSITION_IN_HOTSEAT_NOT_FOUND} if there's no calculation relative to hotseat. + */ + @VisibleForTesting + float getPositionInHotseat(int numShownHotseatIcons, View child, boolean isRtl, + boolean isAllAppsButton, boolean isTaskbarDividerView, boolean isDividerForRecents, + int recentTaskIndex) { + float positionInHotseat; + // Note that there is no All Apps button in the hotseat, + // this position is only used as it's convenient for animation purposes. + float allAppsButtonPositionInHotseat = isRtl + // Right after all hotseat items. + // [HHHHHH]|[>A<] + ? numShownHotseatIcons + // Right before all hotseat items. + // [>A<]|[HHHHHH] + : -1; + // Note that there are no recent tasks in the hotseat, + // this position is only used as it's convenient for animation purposes. + float firstRecentTaskPositionInHotseat = isRtl + // After all hotseat icons and All Apps button. + // [HHHHHH][A]|[>RR 0 + ? relativePosition - DIVIDER_VIEW_POSITION_OFFSET + : relativePosition + DIVIDER_VIEW_POSITION_OFFSET; + } else if (child.getTag() instanceof ItemInfo) { + positionInHotseat = ((ItemInfo) child.getTag()).screenId; + } else if (recentTaskIndex >= 0) { + positionInHotseat = firstRecentTaskPositionInHotseat + recentTaskIndex; + } else { + Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child); + return ERROR_POSITION_IN_HOTSEAT_NOT_FOUND; + } + return positionInHotseat; + } + private boolean bubbleBarHasBubbles() { - return mControllers.bubbleControllers.isPresent() - && mControllers.bubbleControllers.get().bubbleBarViewController.hasBubbles(); + return mBubbleControllers != null + && mBubbleControllers.bubbleBarViewController.hasBubbles(); } public void onRotationChanged(DeviceProfile deviceProfile) { @@ -837,17 +1255,19 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar ? R.dimen.taskbar_phone_size : R.dimen.taskbar_stashed_size); } else { - taskbarWindowSize = deviceProfile.taskbarHeight + deviceProfile.getTaskbarOffsetY(); + taskbarWindowSize = mActivity.getDefaultTaskbarWindowSize(); + } + if (mBubbleControllers != null) { + int bubbleBarMaxHeight = mBubbleControllers.bubbleBarViewController + .getBubbleBarWithFlyoutMaximumHeight(); + taskbarWindowSize = Math.max(taskbarWindowSize, bubbleBarMaxHeight); } mActivity.setTaskbarWindowSize(taskbarWindowSize); mTaskbarNavButtonTranslationY.updateValue(-deviceProfile.getTaskbarOffsetY()); } - /** - * Maps the given operator to all the top-level children of TaskbarView. - */ - public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) { - mTaskbarView.mapOverItems(op); + public LauncherBindableItemsContainer getContent() { + return mModelCallbacks; } /** @@ -857,8 +1277,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar * 3) All Apps button */ public View getFirstIconMatch(Predicate matcher) { - Predicate collectionMatcher = ItemInfoMatcher.forFolderMatch(matcher); - return mTaskbarView.getFirstMatch(matcher, collectionMatcher); + View icon = mModelCallbacks.getFirstMatch(matcher, ItemInfoMatcher.forFolderMatch(matcher)); + return icon != null ? icon : mTaskbarView.getAllAppsButtonContainer(); } /** @@ -869,10 +1289,126 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar return mTaskbarView.isEventOverAnyItem(ev); } + /** Called when there's a change in running apps to update the UI. */ + public void commitRunningAppsToUI() { + mModelCallbacks.commitRunningAppsToUI(); + if (ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() + && !mActivity.isTransientTaskbar() + && mTaskbarView.getLayoutTransition() == null) { + // Set up after the first commit so that the initial recents do not animate (janky). + mTaskbarView.setLayoutTransition(createLayoutTransitionForRunningApps()); + } + } + + private LayoutTransition createLayoutTransitionForRunningApps() { + LayoutTransition layoutTransition = new LayoutTransition(); + layoutTransition.setDuration(TRANSITION_DEFAULT_DURATION); + layoutTransition.addTransitionListener(new TransitionListener() { + + @Override + public void startTransition( + LayoutTransition transition, ViewGroup container, View view, int type) { + if (type == APPEARING) { + view.setAlpha(0f); + view.setScaleX(0f); + view.setScaleY(0f); + } else if (type == DISAPPEARING && view instanceof BubbleTextView btv) { + // Running state updates happen after removing this view, so update it here. + updateRunningState(btv); + } + } + + @Override + public void endTransition( + LayoutTransition transition, ViewGroup container, View view, int type) { + // Do nothing. + } + }); + layoutTransition.addTransitionListener(mTransitionEndBoundsChangedNotifier); + + // Appearing. + AnimatorSet appearingSet = new AnimatorSet(); + Animator appearingAlphaAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); + appearingAlphaAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0f, + (float) TRANSITION_FADE_IN_DURATION / TRANSITION_DEFAULT_DURATION)); + Animator appearingScaleAnimator = ObjectAnimator.ofFloat(null, SCALE_PROPERTY, 0f, 1f); + appearingScaleAnimator.setInterpolator(EMPHASIZED); + appearingSet.playTogether(appearingAlphaAnimator, appearingScaleAnimator); + layoutTransition.setAnimator(APPEARING, appearingSet); + layoutTransition.setStartDelay(APPEARING, TRANSITION_DELAY); + + // Disappearing. + AnimatorSet disappearingSet = new AnimatorSet(); + Animator disappearingAlphaAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); + disappearingAlphaAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, + (float) TRANSITION_DELAY / TRANSITION_DEFAULT_DURATION, + (float) (TRANSITION_DELAY + TRANSITION_FADE_OUT_DURATION) + / TRANSITION_DEFAULT_DURATION)); + Animator disappearingScaleAnimator = ObjectAnimator.ofFloat(null, SCALE_PROPERTY, 1f, 0f); + disappearingScaleAnimator.setInterpolator(EMPHASIZED); + disappearingSet.playTogether(disappearingAlphaAnimator, disappearingScaleAnimator); + layoutTransition.setAnimator(DISAPPEARING, disappearingSet); + + // Change transitions. + FloatProperty translateXPinning = new FloatProperty<>("translateXPinning") { + @Override + public void setValue(View view, float value) { + getTranslationXForPinning(view).setValue(value); + } + + @Override + public Float get(View view) { + return getTranslationXForPinning(view).getValue(); + } + + private MultiProperty getTranslationXForPinning(View view) { + return ((Reorderable) view).getTranslateDelegate() + .getTranslationX(INDEX_TASKBAR_PINNING_ANIM); + } + }; + AnimatorSet changeSet = new AnimatorSet(); + changeSet.playTogether( + layoutTransition.getAnimator(CHANGE_APPEARING), + ObjectAnimator.ofFloat(null, translateXPinning, 0f, 1f)); + + // Change appearing. + layoutTransition.setAnimator(CHANGE_APPEARING, changeSet); + layoutTransition.setInterpolator(CHANGE_APPEARING, EMPHASIZED); + + // Change disappearing. + layoutTransition.setAnimator(CHANGE_DISAPPEARING, changeSet); + layoutTransition.setInterpolator(CHANGE_DISAPPEARING, EMPHASIZED); + layoutTransition.setStartDelay(CHANGE_DISAPPEARING, TRANSITION_DELAY); + + return layoutTransition; + } + + public boolean isTaskbarInMinimalState() { + return mTaskbarView.isTaskbarInMinimalState(); + } + + /** + * To be called when the given Task is updated, so that we can tell TaskbarView to also update. + * @param task The Task whose e.g. icon changed. + */ + public void onTaskUpdated(Task task) { + // Find the icon view(s) that changed. + for (View view : mTaskbarView.getIconViews()) { + if (view instanceof BubbleTextView btv + && view.getTag() instanceof GroupTask groupTask) { + if (groupTask.containsTask(task.key.id)) { + mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask); + } + } else if (view instanceof TaskbarOverflowView overflowButton) { + overflowButton.updateTaskIsShown(task); + } + } + } + @Override public void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "TaskbarViewController:"); - + pw.println(prefix + "\tignoreTaskbarIconCount=" + mTaskbarView.getIgnoreTaskbarIconCount()); mTaskbarIconAlpha.dump( prefix + "\t", pw, @@ -883,20 +1419,34 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar "ALPHA_INDEX_RECENTS_DISABLED", "ALPHA_INDEX_NOTIFICATION_EXPANDED", "ALPHA_INDEX_ASSISTANT_INVOKED", - "ALPHA_INDEX_IME_BUTTON_NAV", "ALPHA_INDEX_SMALL_SCREEN"); mModelCallbacks.dumpLogs(prefix + "\t", pw); } - /** Called when there's a change in running apps to update the UI. */ - public void commitRunningAppsToUI() { - mModelCallbacks.commitRunningAppsToUI(); + private ObjectAnimator createTaskbarIconsShiftAnimator(float translationX) { + ObjectAnimator animator = mIconsTranslationXForNavbar.animateToValue(translationX); + animator.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS); + animator.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS); + animator.setInterpolator(EMPHASIZED); + return animator; } - /** Call TaskbarModelCallbacks to update running apps. */ - public void updateRunningApps() { - mModelCallbacks.updateRunningApps(); - } + private class TransitionEndBoundsChangedNotifier implements TransitionListener { + private boolean mIsCanceled; + @Override + public void startTransition( + LayoutTransition transition, ViewGroup container, View view, int type) { + // Do nothing. + } + + @Override + public void endTransition( + LayoutTransition transition, ViewGroup container, View view, int type) { + if (!transition.isRunning() && !mIsCanceled) { + mControllers.uiController.onIconLayoutBoundsChanged(); + } + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/Utilities.java b/quickstep/src/com/android/launcher3/taskbar/Utilities.java index 47d6684727..9182e03175 100644 --- a/quickstep/src/com/android/launcher3/taskbar/Utilities.java +++ b/quickstep/src/com/android/launcher3/taskbar/Utilities.java @@ -16,12 +16,18 @@ package com.android.launcher3.taskbar; +import static com.android.launcher3.Utilities.dpToPx; + +import com.android.launcher3.graphics.ThemeManager; +import com.android.launcher3.taskbar.customization.TaskbarIconSpecs; + /** * Various utilities shared amongst the Taskbar's classes. */ public final class Utilities { - private Utilities() {} + private Utilities() { + } /** * Sets drag, long-click, and split selection behavior on 1P and 3P launchers with Taskbar @@ -36,4 +42,22 @@ public final class Utilities { controllers.taskbarPopupController.setAllowInitialSplitSelection( allowInitialSplitSelection); } + + /** + * Gives radius for Transient Taskbar based on selected Launcher Icon Shape. + * Transient Taskbar radius = (icon shape radius * icon size ratio) + padding. + * + * @return The radius for Transient Taskbar. + */ + static float getShapedTaskbarRadius(TaskbarActivityContext activityContext) { + float taskbarIconSize = + activityContext.getTaskbarSpecsEvaluator().getTaskbarIconSize().getSize(); + float maxIconSize = TaskbarIconSpecs.INSTANCE.getIconSize52dp().getSize(); + float iconShapeRadius = + ThemeManager.INSTANCE.get(activityContext).getIconState().getShapeRadius(); + float iconSizeRatio = taskbarIconSize / maxIconSize; + return dpToPx((iconShapeRadius * iconSizeRatio) + + TaskbarIconSpecs.INSTANCE.getDefaultTransientIconMargin().getSize(), + activityContext); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt index 5a5ff8e880..c065d35528 100644 --- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt @@ -22,7 +22,6 @@ import android.view.ViewTreeObserver import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION import android.view.WindowManager import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY -import com.android.launcher3.util.DisplayController import com.android.launcher3.views.BaseDragLayer import com.android.systemui.animation.ViewRootSync import java.io.PrintWriter @@ -41,7 +40,7 @@ private const val TEMP_BACKGROUND_WINDOW_TITLE = "VoiceInteractionTaskbarBackgro class VoiceInteractionWindowController(val context: TaskbarActivityContext) : TaskbarControllers.LoggableTaskbarController, TaskbarControllers.BackgroundRendererController { - private val isSeparateBackgroundEnabled = !DisplayController.isTransientTaskbar(context) + private val isSeparateBackgroundEnabled = !context.isTransientTaskbar && !context.isPhoneMode private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context) private val nonTouchableInsetsComputer = ViewTreeObserver.OnComputeInternalInsetsListener { @@ -68,6 +67,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) : separateWindowForTaskbarBackground = object : BaseDragLayer(context, null, 0) { override fun recreateControllers() { + super.recreateControllers() mControllers = emptyArray() } @@ -96,7 +96,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) : separateWindowLayoutParams = context.createDefaultWindowLayoutParams( TYPE_APPLICATION_OVERLAY, - TEMP_BACKGROUND_WINDOW_TITLE + TEMP_BACKGROUND_WINDOW_TITLE, ) separateWindowLayoutParams?.isSystemApplicationOverlay = true } @@ -109,7 +109,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) : } fun setIsVoiceInteractionWindowVisible(visible: Boolean, skipAnim: Boolean) { - if (isVoiceInteractionWindowVisible == visible) { + if (isVoiceInteractionWindowVisible == visible || context.isPhoneMode) { return } isVoiceInteractionWindowVisible = visible @@ -162,7 +162,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) : // First add the temporary window, then hide the overlapping taskbar background. context.addWindowView( separateWindowForTaskbarBackground, - separateWindowLayoutParams + separateWindowLayoutParams, ); { controllers.taskbarDragLayerController.setIsBackgroundDrawnElsewhere(true) } } else { @@ -178,7 +178,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) : ViewRootSync.synchronizeNextDraw( separateWindowForTaskbarBackground!!, context.dragLayer, - onWindowsSynchronized + onWindowsSynchronized, ) } } diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java index 05f1a2f2fb..a46845affa 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsContainerView.java @@ -17,13 +17,10 @@ package com.android.launcher3.taskbar.allapps; import android.content.Context; import android.util.AttributeSet; -import android.view.View; import androidx.annotation.Nullable; -import com.android.launcher3.R; import com.android.launcher3.allapps.ActivityAllAppsContainerView; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; import java.util.Optional; @@ -46,23 +43,6 @@ public class TaskbarAllAppsContainerView extends mOnInvalidateHeaderListener = onInvalidateHeaderListener; } - @Override - protected View inflateSearchBar() { - if (isSearchSupported()) { - return super.inflateSearchBar(); - } - - // Remove top padding of header, since we do not have any search - mHeader.setPadding(mHeader.getPaddingLeft(), 0, - mHeader.getPaddingRight(), mHeader.getPaddingBottom()); - - TaskbarAllAppsFallbackSearchContainer searchView = new TaskbarAllAppsFallbackSearchContainer(getContext(), - null); - searchView.setId(R.id.search_container_all_apps); - searchView.setVisibility(GONE); - return searchView; - } - @Override public void invalidateHeader() { super.invalidateHeader(); @@ -70,11 +50,6 @@ public class TaskbarAllAppsContainerView extends OnInvalidateHeaderListener::onInvalidateHeader); } - @Override - protected boolean isSearchSupported() { - return FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get(); - } - @Override public boolean isInAllApps() { // All apps is always open diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java index 24c0e89713..7ada818860 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java @@ -35,21 +35,15 @@ import com.android.launcher3.util.PackageUserKey; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.function.Predicate; - /** * Handles the all apps overlay window initialization, updates, and its data. *

- * All apps is in an application overlay window instead of taskbar's navigation - * bar panel window, - * because a navigation bar panel is higher than UI components that all apps - * should be below such as + * All apps is in an application overlay window instead of taskbar's navigation bar panel window, + * because a navigation bar panel is higher than UI components that all apps should be below such as * the notification tray. *

- * The all apps window is created and destroyed upon opening and closing all - * apps, respectively. - * Application data may be bound while the window does not exist, so this - * controller will store + * The all apps window is created and destroyed upon opening and closing all apps, respectively. + * Application data may be bound while the window does not exist, so this controller will store * the models for the next all apps session. */ public final class TaskbarAllAppsController { @@ -75,8 +69,7 @@ public final class TaskbarAllAppsController { mControllers = controllers; /* - * Recreate All Apps if it was open in the previous Taskbar instance (e.g. the - * configuration + * Recreate All Apps if it was open in the previous Taskbar instance (e.g. the configuration * changed). */ if (allAppsVisible) { @@ -126,25 +119,12 @@ public final class TaskbarAllAppsController { mZeroStateSearchSuggestions = zeroStateSearchSuggestions; } - /** Updates the current notification dots. */ - public void updateNotificationDots(Predicate updatedDots) { - if (mAppsView != null) { - mAppsView.getAppsStore().updateNotificationDots(updatedDots); - } - } - - /** - * Toggles visibility of {@link TaskbarAllAppsContainerView} in the overlay - * window. - */ + /** Toggles visibility of {@link TaskbarAllAppsContainerView} in the overlay window. */ public void toggle() { toggle(false); } - /** - * Toggles visibility of {@link TaskbarAllAppsContainerView} with the keyboard - * for search. - */ + /** Toggles visibility of {@link TaskbarAllAppsContainerView} with the keyboard for search. */ public void toggleSearch() { toggle(true); } @@ -153,6 +133,8 @@ public final class TaskbarAllAppsController { if (isOpen()) { mSlideInView.close(true); } else { + mControllers.taskbarEduTooltipController.hide(); + mControllers.taskbarPopupController.maybeCloseMultiInstanceMenu(); show(true, showKeyboard); } } @@ -170,11 +152,6 @@ public final class TaskbarAllAppsController { if (mAppsView != null) { return; } - // mControllers and getSharedState should never be null here. Do not handle - // null-pointer - // to catch invalid states. - mControllers.getSharedState().allAppsVisible = true; - mOverlayContext = mControllers.taskbarOverlayController.requestWindow(); // Initialize search session for All Apps. @@ -188,10 +165,8 @@ public final class TaskbarAllAppsController { mSlideInView = (TaskbarAllAppsSlideInView) mOverlayContext.getLayoutInflater().inflate( R.layout.taskbar_all_apps_sheet, mOverlayContext.getDragLayer(), false); - // Ensures All Apps gets touch events in case it is not the top floating view. - // Floating - // views above it may not be able to intercept the touch, so All Apps should try - // to. + // Ensures All Apps gets touch events in case it is not the top floating view. Floating + // views above it may not be able to intercept the touch, so All Apps should try to. mOverlayContext.getDragLayer().addTouchController(mSlideInView); mSlideInView.addOnCloseListener(this::cleanUpOverlay); TaskbarAllAppsViewController viewController = new TaskbarAllAppsViewController( @@ -208,18 +183,15 @@ public final class TaskbarAllAppsController { .findFixedRowByType(PredictionRowView.class) .setPredictedApps(mPredictedApps); // 1 alternative that would be more work: - // Create a shared drag layer between taskbar and taskbarAllApps so that when - // dragging - // starts and taskbarAllApps can close, but the drag layer that the view is - // being dragged in + // Create a shared drag layer between taskbar and taskbarAllApps so that when dragging + // starts and taskbarAllApps can close, but the drag layer that the view is being dragged in // doesn't also close mOverlayContext.getDragController().setDisallowGlobalDrag(mDisallowGlobalDrag); mOverlayContext.getDragController().setDisallowLongClick(mDisallowLongClick); } private void cleanUpOverlay() { - // Floating search bar is added to the drag layer in - // ActivityAllAppsContainerView onAttach; + // Floating search bar is added to the drag layer in ActivityAllAppsContainerView onAttach; // removed here as this is a special case that we remove the all apps panel. if (mAppsView != null && mOverlayContext != null && mAppsView.getSearchUiDelegate().isSearchBarFloating()) { @@ -239,17 +211,20 @@ public final class TaskbarAllAppsController { mAppsView = null; } + @Nullable + public TaskbarAllAppsContainerView getAppsView() { + return mAppsView; + } + @VisibleForTesting public int getTaskbarAllAppsTopPadding() { - // Allow null-pointer since this should only be null if the apps view is not - // showing. + // Allow null-pointer since this should only be null if the apps view is not showing. return mAppsView.getActiveRecyclerView().getClipBounds().top; } @VisibleForTesting public int getTaskbarAllAppsScroll() { - // Allow null-pointer since this should only be null if the apps view is not - // showing. + // Allow null-pointer since this should only be null if the apps view is not showing. return mAppsView.getActiveRecyclerView().computeVerticalScrollOffset(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java deleted file mode 100644 index 53fe06d32c..0000000000 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsFallbackSearchContainer.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.taskbar.allapps; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.Nullable; - -import com.android.launcher3.ExtendedEditText; -import com.android.launcher3.allapps.ActivityAllAppsContainerView; -import com.android.launcher3.allapps.SearchUiManager; - -/** Empty search container for Taskbar All Apps used as a fallback if search is not supported. */ -public class TaskbarAllAppsFallbackSearchContainer extends View implements SearchUiManager { - public TaskbarAllAppsFallbackSearchContainer(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public TaskbarAllAppsFallbackSearchContainer( - Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - public void initializeSearch(ActivityAllAppsContainerView containerView) { - // Do nothing. - } - - @Override - public void resetSearch() { - // Do nothing. - } - - @Nullable - @Override - public ExtendedEditText getEditText() { - return null; - } -} diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java index 39cd5ef31b..4a0aeabe6f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java @@ -15,10 +15,14 @@ */ package com.android.launcher3.taskbar.allapps; +import static android.os.Trace.TRACE_TAG_APP; + +import static com.android.app.animation.Interpolators.DECELERATED_EASE; import static com.android.app.animation.Interpolators.EMPHASIZED; -import static com.android.launcher3.Flags.enablePredictiveBackGesture; +import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.touch.AllAppsSwipeController.ALL_APPS_FADE_MANUAL; import static com.android.launcher3.touch.AllAppsSwipeController.SCRIM_FADE_MANUAL; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import android.animation.Animator; import android.content.Context; @@ -26,9 +30,13 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; +import android.os.Trace; import android.util.AttributeSet; +import android.util.Log; +import android.view.CrossWindowBlurListeners; import android.view.MotionEvent; import android.view.View; +import android.view.ViewRootImpl; import android.view.animation.Interpolator; import android.window.OnBackInvokedDispatcher; @@ -36,24 +44,31 @@ import androidx.annotation.Nullable; import com.android.app.animation.Interpolators; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.Insettable; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks; import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; import com.android.launcher3.util.Themes; import com.android.launcher3.views.AbstractSlideInView; +import java.util.function.Consumer; + /** Wrapper for taskbar all apps with slide-in behavior. */ public class TaskbarAllAppsSlideInView extends AbstractSlideInView implements Insettable, DeviceProfile.OnDeviceProfileChangeListener { + private static final String TAG = "TaskbarAllAppsSlideInView"; + private final Handler mHandler; + private final int mMaxBlurRadius; + private final Consumer mWindowBlurListener = blursEnabled -> invalidate(); private TaskbarAllAppsContainerView mAppsView; private float mShiftRange; + private int mBlurRadius; private @Nullable Runnable mShowOnFullyAttachedToWindowRunnable; // Initialized in init. @@ -67,6 +82,8 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView { + float blurProgress = + isOpening ? a.getAnimatedFraction() : 1 - a.getAnimatedFraction(); + mBlurRadius = + (int) (mMaxBlurRadius * blurInterpolator.getInterpolation(blurProgress)); + }); + } + mAllAppsCallbacks.onAllAppsAnimationPending(animation, isOpening); } @Override protected Interpolator getScrimInterpolator() { - if (mActivityContext.getDeviceProfile().isTablet) { + if (mActivityContext.getDeviceProfile().getDeviceProperties().isTablet()) { return super.getScrimInterpolator(); } return mToTranslationShift == TRANSLATION_SHIFT_OPENED @@ -176,15 +214,13 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView - * For details around the behavior of the bubble bar, see {@link BubbleBarView}. + *

For details around the behavior of the bubble bar, see {@link BubbleBarView}. */ public class BubbleBarController extends IBubblesListener.Stub { @@ -101,8 +72,7 @@ public class BubbleBarController extends IBubblesListener.Stub { private static final boolean DEBUG = false; /** - * Determines whether bubbles can be shown in the bubble bar. This value updates - * when the + * Determines whether bubbles can be shown in the bubble bar. This value updates when the * taskbar is recreated. * * @see #onTaskbarRecreated() @@ -115,10 +85,7 @@ public class BubbleBarController extends IBubblesListener.Stub { return sBubbleBarEnabled; } - /** - * Re-reads the value of the flag from SystemProperties when taskbar is - * recreated. - */ + /** Re-reads the value of the flag from SystemProperties when taskbar is recreated. */ public static void onTaskbarRecreated() { sBubbleBarEnabled = Flags.enableBubbleBar() || SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); @@ -127,14 +94,14 @@ public class BubbleBarController extends IBubblesListener.Stub { private static final long MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED - | SYSUI_STATE_IME_SHOWING + | SYSUI_STATE_IME_VISIBLE | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED - | SYSUI_STATE_QUICK_SETTINGS_EXPANDED - | SYSUI_STATE_IME_SWITCHER_SHOWING; + | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; private static final long MASK_HIDE_HANDLE_VIEW = SYSUI_STATE_BOUNCER_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING - | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED + | SYSUI_STATE_IME_VISIBLE; private static final long MASK_SYSUI_LOCKED = SYSUI_STATE_BOUNCER_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING @@ -143,28 +110,28 @@ public class BubbleBarController extends IBubblesListener.Stub { private final Context mContext; private final BubbleBarView mBarView; private final ArrayMap mBubbles = new ArrayMap<>(); + private final ArrayMap mSuppressedBubbles = new ArrayMap<>(); private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor( new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND)); - private final LauncherApps mLauncherApps; - private final BubbleIconFactory mIconFactory; private final SystemUiProxy mSystemUiProxy; private BubbleBarItem mSelectedBubble; - private BubbleBarOverflow mOverflowBubble; - private ImeVisibilityChecker mImeVisibilityChecker; + private TaskbarSharedState mSharedState; private BubbleBarViewController mBubbleBarViewController; private BubbleStashController mBubbleStashController; - private BubbleStashedHandleViewController mBubbleStashedHandleViewController; + private Optional mBubbleStashedHandleViewController; private BubblePinController mBubblePinController; + private BubbleCreator mBubbleCreator; + private BubbleBarLocationListener mBubbleBarLocationListener; - // Cache last sent top coordinate to avoid sending duplicate updates to shell - private int mLastSentBubbleBarTop; + private int mLastSentBubbleBarTopToScreenBottom; + + private boolean mIsImeVisible = false; /** - * Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it - * uses + * Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it uses * {@link BubbleBarBubble}s so that it can be used to update the views. */ private static class BubbleBarViewUpdate { @@ -179,6 +146,8 @@ public class BubbleBarController extends IBubblesListener.Stub { List removedBubbles; List bubbleKeysInOrder; Point expandedViewDropTargetSize; + boolean showOverflow; + boolean showOverflowChanged; // These need to be loaded in the background BubbleBarBubble addedBubble; @@ -197,6 +166,8 @@ public class BubbleBarController extends IBubblesListener.Stub { removedBubbles = update.removedBubbles; bubbleKeysInOrder = update.bubbleKeysInOrder; expandedViewDropTargetSize = update.expandedViewDropTargetSize; + showOverflow = update.showOverflow; + showOverflowChanged = update.showOverflowChanged; } } @@ -205,81 +176,89 @@ public class BubbleBarController extends IBubblesListener.Stub { mBarView = bubbleView; // Need the view for inflating bubble views. mSystemUiProxy = SystemUiProxy.INSTANCE.get(context); - - if (sBubbleBarEnabled) { - mSystemUiProxy.setBubblesListener(this); - } - mLauncherApps = context.getSystemService(LauncherApps.class); - mIconFactory = new BubbleIconFactory(context, - context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size), - context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size), - context.getResources().getColor(R.color.important_conversation), - context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.importance_ring_stroke_width)); } public void onDestroy() { mSystemUiProxy.setBubblesListener(null); + // Saves bubble bar state + mSharedState.bubbleBarExpanded = mBubbleBarViewController.isExpanded(); + mSharedState.bubbleBarStashed = mBubbleStashController.isStashed(); + mSharedState.selectedBubbleKey = mSelectedBubble != null ? mSelectedBubble.getKey() : null; + BubbleInfo[] bubbleInfoItems = new BubbleInfo[mBubbles.size()]; + mBubbles.values().forEach(bubbleBarBubble -> { + int index = mBubbleBarViewController.bubbleViewIndex(bubbleBarBubble.getView()); + if (index < 0 || index >= bubbleInfoItems.length) { + Log.e(TAG, "Found improper index: " + index + " for " + bubbleBarBubble); + } else { + bubbleInfoItems[index] = bubbleBarBubble.getInfo(); + } + }); + mSharedState.bubbleInfoItems = Arrays.asList(bubbleInfoItems); + mSharedState.suppressedBubbleInfoItems = new ArrayList<>(mSuppressedBubbles.size()); + for (int i = 0; i < mSuppressedBubbles.size(); i++) { + mSharedState.suppressedBubbleInfoItems.add(mSuppressedBubbles.valueAt(i).getInfo()); + } } /** Initializes controllers. */ public void init(BubbleControllers bubbleControllers, - ImeVisibilityChecker imeVisibilityChecker) { - mImeVisibilityChecker = imeVisibilityChecker; + BubbleBarLocationListener bubbleBarLocationListener, + TaskbarSharedState sharedState) { + mSharedState = sharedState; mBubbleBarViewController = bubbleControllers.bubbleBarViewController; mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController; mBubblePinController = bubbleControllers.bubblePinController; + mBubbleCreator = bubbleControllers.bubbleCreator; + mBubbleBarLocationListener = bubbleBarLocationListener; bubbleControllers.runAfterInit(() -> { + restoreSavedState(sharedState); mBubbleBarViewController.setHiddenForBubbles( !sBubbleBarEnabled || mBubbles.isEmpty()); - mBubbleStashedHandleViewController.setHiddenForBubbles( - !sBubbleBarEnabled || mBubbles.isEmpty()); + mBubbleStashedHandleViewController.ifPresent( + controller -> controller.setHiddenForBubbles( + !sBubbleBarEnabled || mBubbles.isEmpty())); mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse( key -> setSelectedBubbleInternal(mBubbles.get(key))); mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged); + mBubbleBarLocationListener.onBubbleBarLocationUpdated( + mBubbleBarViewController.getBubbleBarLocation()); + if (sBubbleBarEnabled) { + mSystemUiProxy.setBubblesListener(this); + mSystemUiProxy.setHasBubbleBar(true); + } }); } /** - * Creates and adds the overflow bubble to the bubble bar if it hasn't been - * created yet. - * - *

- * This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid - * inflating - * the overflow multiple times. - */ - private void createAndAddOverflowIfNeeded() { - if (mOverflowBubble == null) { - BubbleBarOverflow overflow = createOverflow(mContext); - MAIN_EXECUTOR.execute(() -> { - // we're on the main executor now, so check that the overflow hasn't been - // created - // again to avoid races. - if (mOverflowBubble == null) { - mBubbleBarViewController.addBubble( - overflow, /* isExpanding= */ false, /* suppressAnimation= */ true); - mOverflowBubble = overflow; - } - }); - } - } - - /** - * Updates the bubble bar, handle bar, and stash controllers based on sysui - * state flags. + * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags. */ public void updateStateForSysuiFlags(@SystemUiStateFlags long flags) { boolean hideBubbleBar = (flags & MASK_HIDE_BUBBLE_BAR) != 0; mBubbleBarViewController.setHiddenForSysui(hideBubbleBar); boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0; - mBubbleStashedHandleViewController.setHiddenForSysui(hideHandleView); + mBubbleStashedHandleViewController.ifPresent(controller -> { + controller.setHiddenForSysui(hideHandleView); + MultiPropertyFactory.MultiProperty handleViewAlpha = + mBubbleStashController.getHandleViewAlpha(); + boolean shouldShowHandleView = handleViewAlpha != null + && !hideHandleView + && mBubbleStashController.isStashed() + && mBubbleBarViewController.hasBubbles(); + if (shouldShowHandleView) { + // TODO: (b/273592694) animate it? + handleViewAlpha.setValue(1f); + } + }); boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0; - mBubbleStashController.onSysuiLockedStateChange(sysuiLocked); + mBubbleStashController.setSysuiLocked(sysuiLocked); + mIsImeVisible = (flags & SYSUI_STATE_IME_VISIBLE) != 0; + if (mIsImeVisible) { + mBubbleBarViewController.onImeVisible(); + } } // @@ -297,20 +276,24 @@ public class BubbleBarController extends IBubblesListener.Stub { || !update.currentBubbleList.isEmpty()) { // We have bubbles to load BUBBLE_STATE_EXECUTOR.execute(() -> { - createAndAddOverflowIfNeeded(); if (update.addedBubble != null) { - viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView, + viewUpdate.addedBubble = mBubbleCreator.populateBubble(mContext, + update.addedBubble, + mBarView, null /* existingBubble */); } if (update.updatedBubble != null) { BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey()); - viewUpdate.updatedBubble = populateBubble(mContext, update.updatedBubble, mBarView, - existingBubble); + viewUpdate.updatedBubble = + mBubbleCreator.populateBubble(mContext, update.updatedBubble, + mBarView, + existingBubble); } if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) { List currentBubbles = new ArrayList<>(); for (int i = 0; i < update.currentBubbleList.size(); i++) { - BubbleBarBubble b = populateBubble(mContext, update.currentBubbleList.get(i), mBarView, + BubbleBarBubble b = mBubbleCreator.populateBubble(mContext, + update.currentBubbleList.get(i), mBarView, null /* existingBubble */); currentBubbles.add(b); } @@ -325,49 +308,163 @@ public class BubbleBarController extends IBubblesListener.Stub { } } + private void restoreSavedState(TaskbarSharedState sharedState) { + if (sharedState.bubbleBarLocation != null) { + updateBubbleBarLocationInternal(sharedState.bubbleBarLocation); + } + List savedBubbles = sharedState.bubbleInfoItems; + boolean hasSavedBubbles = savedBubbles != null && !savedBubbles.isEmpty(); + if (hasSavedBubbles) { + restoreSavedBubbles(savedBubbles); + } + restoreSuppressed(sharedState.suppressedBubbleInfoItems); + if (hasSavedBubbles) { + setSelectedBubbleInternal(mBubbles.get(sharedState.selectedBubbleKey)); + if (sharedState.bubbleBarExpanded) { + // We don't want state restore to have side effects which update the Shell state. + // Use the method for setting expanded state from sysui as that won't trigger an + // update back to Shell. + mBubbleBarViewController.setExpandedFromSysui(/* isExpanded= */ true, + /* animate= */ false); + } else if (sharedState.bubbleBarStashed) { + mBubbleStashController.stashBubbleBarImmediate(); + } else { + mBubbleStashController.showBubbleBarImmediate(); + } + } + } + + private void restoreSavedBubbles(List bubbleInfos) { + // Iterate in reverse because new bubbles are added in front and the list is in order. + for (int i = bubbleInfos.size() - 1; i >= 0; i--) { + BubbleBarBubble bubble = mBubbleCreator.populateBubble(mContext, + bubbleInfos.get(i), mBarView, /* existingBubble = */ null); + if (bubble == null) { + Log.e(TAG, "Could not instantiate BubbleBarBubble for " + bubbleInfos.get(i)); + continue; + } + mBubbles.put(bubble.getKey(), bubble); + mBubbleBarViewController.restoreBubble(bubble); + } + } + + private void restoreSuppressed(List bubbleInfos) { + if (bubbleInfos == null || bubbleInfos.isEmpty()) return; + for (BubbleInfo bubbleInfo : bubbleInfos.reversed()) { + BubbleBarBubble bb = mBubbleCreator.populateBubble(mContext, bubbleInfo, + mBarView, /* existingBubble= */ + null); + if (bb != null) { + mSuppressedBubbles.put(bb.getKey(), bb); + } + } + } + private void applyViewChanges(BubbleBarViewUpdate update) { + if (update.initialState) { + // it is possible that we tried to notify shell too early with the bubble bar bounds, + // so force update shell about the bubble bar bounds in the initial handshake. + onBubbleBarBoundsChanged(/* forceUpdate= */ true); + } final boolean isCollapsed = (update.expandedChanged && !update.expanded) || (!update.expandedChanged && !mBubbleBarViewController.isExpanded()); final boolean isExpanding = update.expandedChanged && update.expanded; - // don't animate bubbles if this is the initial state because we may be - // unfolding or - // enabling gesture nav. also suppress animation if the bubble bar is hidden for - // sysui e.g. + // don't animate bubbles if this is the initial state because we may be unfolding or + // enabling gesture nav. also suppress animation if the bubble bar is hidden for sysui e.g. // the shade is open, or we're locked. - final boolean suppressAnimation = update.initialState || mBubbleBarViewController.isHiddenForSysui() - || mImeVisibilityChecker.isImeVisible(); + final boolean suppressAnimation = + update.initialState || mBubbleBarViewController.isHiddenForSysui() || mIsImeVisible; + if (update.initialState && mSharedState.hasSavedBubbles()) { + // clear restored state + mBubbleBarViewController.removeAllBubbles(); + mBubbles.clear(); + mBubbleBarViewController.showOverflow(update.showOverflow); + } + + if (update.addedBubble != null) { + mBubbles.put(update.addedBubble.getKey(), update.addedBubble); + } BubbleBarBubble bubbleToSelect = null; - if (!update.removedBubbles.isEmpty()) { - for (int i = 0; i < update.removedBubbles.size(); i++) { - RemovedBubble removedBubble = update.removedBubbles.get(i); - BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey()); - if (bubble != null) { - mBubbleBarViewController.removeBubble(bubble); - } else { - Log.w(TAG, "trying to remove bubble that doesn't exist: " - + removedBubble.getKey()); + if (update.selectedBubbleKey != null) { + if (mSelectedBubble == null + || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) { + BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey); + if (newlySelected != null) { + bubbleToSelect = newlySelected; } } } - if (update.addedBubble != null) { - mBubbles.put(update.addedBubble.getKey(), update.addedBubble); - mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation); - if (isCollapsed) { - // If we're collapsed, the most recently added bubble will be selected. - bubbleToSelect = update.addedBubble; + if (Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null + && update.removedBubbles.isEmpty() + && !mBubbles.isEmpty()) { + // A bubble was added from the overflow (& now it's empty / not showing) + mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble, bubbleToSelect); + } else if (update.addedBubble != null && update.removedBubbles.size() == 1) { + // we're adding and removing a bubble at the same time. handle this as a single update. + RemovedBubble removedBubble = update.removedBubbles.get(0); + BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey()); + boolean showOverflow = update.showOverflowChanged && update.showOverflow; + if (bubbleToRemove != null) { + mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble, + bubbleToRemove, bubbleToSelect, isExpanding, suppressAnimation, + showOverflow); + } else { + mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, + suppressAnimation, bubbleToSelect); + Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey()); + } + } else { + boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged && update.showOverflow; + if (!update.removedBubbles.isEmpty()) { + for (int i = 0; i < update.removedBubbles.size(); i++) { + RemovedBubble removedBubble = update.removedBubbles.get(i); + BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey()); + if (bubble != null && overflowNeedsToBeAdded) { + // First removal, show the overflow + overflowNeedsToBeAdded = false; + mBubbleBarViewController.addOverflowAndRemoveBubble(bubble, bubbleToSelect); + } else if (bubble != null) { + mBubbleBarViewController.removeBubble(bubble); + } else { + Log.w(TAG, "trying to remove bubble that doesn't exist: " + + removedBubble.getKey()); + } + } + } + if (update.addedBubble != null) { + mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, + suppressAnimation, bubbleToSelect); + } + if (Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged + && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) { + mBubbleBarViewController.showOverflow(update.showOverflow); } - } + + // if a bubble was updated upstream, but removed before the update was received, add it back + if (update.updatedBubble != null && !mBubbles.containsKey(update.updatedBubble.getKey())) { + addBubbleInternally(update.updatedBubble, isExpanding, suppressAnimation); + } + + if (update.addedBubble != null && isCollapsed && bubbleToSelect == null) { + // If we're collapsed, the most recently added bubble will be selected. + bubbleToSelect = update.addedBubble; + } + if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) { - // Iterate in reverse because new bubbles are added in front and the list is in - // order. + // Iterate in reverse because new bubbles are added in front and the list is in order. for (int i = update.currentBubbles.size() - 1; i >= 0; i--) { BubbleBarBubble bubble = update.currentBubbles.get(i); if (bubble != null) { - mBubbles.put(bubble.getKey(), bubble); - mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation); - if (isCollapsed) { + if (bubble.getKey().equals(update.selectedBubbleKey)) { + bubbleToSelect = bubble; + } + addBubbleInternally(bubble, isExpanding, suppressAnimation); + if (isCollapsed && bubbleToSelect == null) { // If we're collapsed, the most recently added bubble will be selected. bubbleToSelect = bubble; } @@ -377,11 +474,37 @@ public class BubbleBarController extends IBubblesListener.Stub { } } } + if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) { + mBubbleBarViewController.showOverflow(true); + } - // Adds and removals have happened, update visibility before any other visual - // changes. - mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty()); - mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty()); + if (update.suppressedBubbleKey != null) { + BubbleBarBubble bb = mBubbles.remove(update.suppressedBubbleKey); + if (bb != null) { + mSuppressedBubbles.put(update.suppressedBubbleKey, bb); + mBubbleBarViewController.removeBubble(bb); + } + } + if (update.unsuppressedBubbleKey != null) { + BubbleBarBubble bb = mSuppressedBubbles.remove(update.unsuppressedBubbleKey); + if (bb != null) { + // Unsuppressing an existing bubble should not cause the bar to expand or animate + addBubbleInternally(bb, /* isExpanding= */ false, /* suppressAnimation= */ true); + if (mBubbleBarViewController.isHiddenForNoBubbles()) { + mBubbleBarViewController.setHiddenForBubbles(false); + } + } + } + + // Update the visibility if this is the initial state, if there are no bubbles, or if the + // animation is suppressed. + // If this is the initial bubble, the bubble bar will become visible as part of the + // animation. + if (update.initialState || mBubbles.isEmpty() || suppressAnimation) { + mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty()); + } + mBubbleStashedHandleViewController.ifPresent( + controller -> controller.setHiddenForBubbles(mBubbles.isEmpty())); if (mBubbles.isEmpty()) { // all bubbles were removed. clear the selected bubble @@ -389,54 +512,46 @@ public class BubbleBarController extends IBubblesListener.Stub { } if (update.updatedBubble != null) { - // Updates mean the dot state may have changed; any other changes were updated - // in + // Updates mean the dot state may have changed; any other changes were updated in // the populateBubble step. BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey()); - // If we're not stashed, we're visible so animate - bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */); - mBubbleBarViewController.animateBubbleNotification(bb, /* isExpanding= */ false); + if (suppressAnimation) { + // since we're not animating this update, we should update the dot visibility here. + bb.getView().updateDotVisibility(/* animate= */ false); + } else { + mBubbleBarViewController.animateBubbleNotification( + bb, /* isExpanding= */ false, /* isUpdate= */ true); + } } if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) { // Create the new list List newOrder = update.bubbleKeysInOrder.stream() - .map(mBubbles::get).filter(Objects::nonNull).collect(toList()); + .map(mBubbles::get).filter(Objects::nonNull).toList(); if (!newOrder.isEmpty()) { mBubbleBarViewController.reorderBubbles(newOrder); } } - if (update.suppressedBubbleKey != null) { - // TODO: (b/273316505) handle suppression - } - if (update.unsuppressedBubbleKey != null) { - // TODO: (b/273316505) handle suppression - } - if (update.selectedBubbleKey != null) { - if (mSelectedBubble == null - || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) { - BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey); - if (newlySelected != null) { - bubbleToSelect = newlySelected; - } else { - Log.w(TAG, "trying to select bubble that doesn't exist:" - + update.selectedBubbleKey); - } - } - } if (bubbleToSelect != null) { setSelectedBubbleInternal(bubbleToSelect); + } else if (update.initialState && BubbleBarOverflow.KEY.equals(update.selectedBubbleKey)) { + // this is the initial update with the overflow selected which could happen after + // unfolding with the overflow expanded + setSelectedBubbleInternal(mBubbleBarViewController.getOverflowBubble()); } if (update.shouldShowEducation) { mBubbleBarViewController.prepareToShowEducation(); } if (update.expandedChanged) { if (update.expanded != mBubbleBarViewController.isExpanded()) { - mBubbleBarViewController.setExpandedFromSysui(update.expanded); + // If we start as expanded, show bar immediately without waiting for animation. + boolean animate = !update.initialState; + mBubbleBarViewController.setExpandedFromSysui(update.expanded, animate); } else { Log.w(TAG, "expansion was changed but is the same"); } } if (update.bubbleBarLocation != null) { + mSharedState.bubbleBarLocation = update.bubbleBarLocation; if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) { updateBubbleBarLocationInternal(update.bubbleBarLocation); } @@ -446,47 +561,38 @@ public class BubbleBarController extends IBubblesListener.Stub { } } + /** + * Removes the given bubble from the backing list of bubbles after it was dismissed by the user. + */ + public void onBubbleDismissed(BubbleView bubble) { + mBubbles.remove(bubble.getBubble().getKey()); + } + /** Tells WMShell to show the currently selected bubble. */ public void showSelectedBubble() { if (getSelectedBubbleKey() != null) { - if (mSelectedBubble instanceof BubbleBarBubble) { - // Because we've visited this bubble, we should suppress the notification. - // This is updated on WMShell side when we show the bubble, but that update - // isn't - // passed to launcher, instead we apply it directly here. - BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo(); - info.setFlags( - info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); - mSelectedBubble.getView().updateDotVisibility(true /* animate */); - } - mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen(); - mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop); + mLastSentBubbleBarTopToScreenBottom = mBarView.getTopToScreenBottom(); + mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTopToScreenBottom); } else { Log.w(TAG, "Trying to show the selected bubble but it's null"); } } - /** - * Updates the currently selected bubble for launcher views and tells WMShell to - * show it. - */ + /** Updates the currently selected bubble for launcher views and tells WMShell to show it. */ public void showAndSelectBubble(BubbleBarItem b) { - if (DEBUG) - Log.w(TAG, "showingSelectedBubble: " + b.getKey()); + if (DEBUG) Log.w(TAG, "showingSelectedBubble: " + b.getKey()); setSelectedBubbleInternal(b); showSelectedBubble(); } /** - * Sets the bubble that should be selected. This notifies the views, it does not - * notify + * Sets the bubble that should be selected. This notifies the views, it does not notify * WMShell that the selection has changed, that should go through either * {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}. */ private void setSelectedBubbleInternal(BubbleBarItem b) { if (!Objects.equals(b, mSelectedBubble)) { - if (DEBUG) - Log.w(TAG, "selectingBubble: " + b.getKey()); + if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey()); mSelectedBubble = b; mBubbleBarViewController.updateSelectedBubble(mSelectedBubble); } @@ -508,164 +614,69 @@ public class BubbleBarController extends IBubblesListener.Stub { *

* Updates the value locally in Launcher and in WMShell. */ - public void updateBubbleBarLocation(BubbleBarLocation location) { + public void updateBubbleBarLocation(BubbleBarLocation location, + @BubbleBarLocation.UpdateSource int source) { updateBubbleBarLocationInternal(location); - mSystemUiProxy.setBubbleBarLocation(location); + mSystemUiProxy.setBubbleBarLocation(location, source); } private void updateBubbleBarLocationInternal(BubbleBarLocation location) { mBubbleBarViewController.setBubbleBarLocation(location); mBubbleStashController.setBubbleBarLocation(location); + mBubbleBarLocationListener.onBubbleBarLocationUpdated(location); } @Override public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { MAIN_EXECUTOR.execute( - () -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation)); + () -> { + mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation); + mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation); + }); + } + + @Override + public void showBubbleBarPillowAt(@Nullable BubbleBarLocation location) { + MAIN_EXECUTOR.execute(() -> { + //TODO(b/411505605) add logic to show pillow and update taskbar + }); + } + + /** Notifies WMShell to show the expanded view. */ + void showExpandedView() { + mSystemUiProxy.showExpandedView(); } // // Loading data for the bubbles // - @Nullable - private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv, - @Nullable BubbleBarBubble existingBubble) { - String appName; - Bitmap badgeBitmap; - Bitmap bubbleBitmap; - Path dotPath; - int dotColor; - - boolean isImportantConvo = b.isImportantConversation(); - - ShortcutRequest.QueryResult result = new ShortcutRequest(context, - new UserHandle(b.getUserId())) - .forPackage(b.getPackageName(), b.getShortcutId()) - .query(FLAG_MATCH_DYNAMIC - | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER - | FLAG_MATCH_CACHED - | FLAG_GET_PERSONS_DATA); - - ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null; - if (shortcutInfo == null) { - Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey() - + " with shortcutId: " + b.getShortcutId()); - } - - ApplicationInfo appInfo; - try { - appInfo = mLauncherApps.getApplicationInfo( - b.getPackageName(), - 0, - new UserHandle(b.getUserId())); - } catch (PackageManager.NameNotFoundException e) { - // If we can't find package... don't think we should show the bubble. - Log.w(TAG, "Unable to find packageName: " + b.getPackageName()); - return null; - } - if (appInfo == null) { - Log.w(TAG, "Unable to find appInfo: " + b.getPackageName()); - return null; - } - PackageManager pm = context.getPackageManager(); - appName = String.valueOf(appInfo.loadLabel(pm)); - Drawable appIcon = appInfo.loadUnbadgedIcon(pm); - Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId())); - - // Badged bubble image - Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo, - b.getIcon()); - if (bubbleDrawable == null) { - // Default to app icon - bubbleDrawable = appIcon; - } - - BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo); - badgeBitmap = badgeBitmapInfo.icon; - - float[] bubbleBitmapScale = new float[1]; - bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); - - // Dot color & placement - Path iconPath = PathParser.createPathFromPathData( - context.getResources().getString( - com.android.internal.R.string.config_icon_mask)); - Matrix matrix = new Matrix(); - float scale = bubbleBitmapScale[0]; - float radius = BubbleView.DEFAULT_PATH_SIZE / 2f; - matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, - radius /* pivot y */); - iconPath.transform(matrix); - dotPath = iconPath; - dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, - Color.WHITE, WHITE_SCRIM_ALPHA / 255f); - - if (existingBubble == null) { - LayoutInflater inflater = LayoutInflater.from(context); - BubbleView bubbleView = (BubbleView) inflater.inflate( - R.layout.bubblebar_item_view, bbv, false /* attachToRoot */); - - BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView, - badgeBitmap, bubbleBitmap, dotColor, dotPath, appName); - bubbleView.setBubble(bubble); - return bubble; - } else { - // If we already have a bubble (so it already has an inflated view), update it. - existingBubble.setInfo(b); - existingBubble.setBadge(badgeBitmap); - existingBubble.setIcon(bubbleBitmap); - existingBubble.setDotColor(dotColor); - existingBubble.setDotPath(dotPath); - existingBubble.setAppName(appName); - return existingBubble; - } - } - - private BubbleBarOverflow createOverflow(Context context) { - Bitmap bitmap = createOverflowBitmap(context); - LayoutInflater inflater = LayoutInflater.from(context); - BubbleView bubbleView = (BubbleView) inflater.inflate( - R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */); - BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView); - bubbleView.setOverflow(overflow, bitmap); - return overflow; - } - - private Bitmap createOverflowBitmap(Context context) { - Drawable iconDrawable = AppCompatResources.getDrawable(mContext, - R.drawable.bubble_ic_overflow_button); - - final TypedArray ta = mContext.obtainStyledAttributes( - new int[] { - com.android.internal.R.attr.materialColorOnPrimaryFixed, - com.android.internal.R.attr.materialColorPrimaryFixed - }); - int overflowIconColor = ta.getColor(0, Color.WHITE); - int overflowBackgroundColor = ta.getColor(1, Color.BLACK); - ta.recycle(); - - iconDrawable.setTint(overflowIconColor); - - int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset); - Drawable foreground = new InsetDrawable(iconDrawable, inset); - Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor), - foreground); - - return mIconFactory.createBadgedIconBitmap(drawable).icon; - } - private void onBubbleBarBoundsChanged() { - int newTop = mBarView.getRestingTopPositionOnScreen(); - if (newTop != mLastSentBubbleBarTop) { - mLastSentBubbleBarTop = newTop; - mSystemUiProxy.updateBubbleBarTopOnScreen(newTop); + onBubbleBarBoundsChanged(/* forceUpdate= */ false); + } + + private void onBubbleBarBoundsChanged(boolean forceUpdate) { + int bubbleBarTopToScreenBottom = mBarView.getTopToScreenBottom(); + if (bubbleBarTopToScreenBottom != mLastSentBubbleBarTopToScreenBottom || forceUpdate) { + mLastSentBubbleBarTopToScreenBottom = bubbleBarTopToScreenBottom; + mSystemUiProxy.updateBubbleBarTopToScreenBottom(bubbleBarTopToScreenBottom); } } - /** Interface for checking whether the IME is visible. */ - public interface ImeVisibilityChecker { - /** Whether the IME is visible. */ - boolean isImeVisible(); + private void addBubbleInternally(BubbleBarBubble bubble, boolean isExpanding, + boolean suppressAnimation) { + mBubbles.put(bubble.getKey(), bubble); + mBubbleBarViewController.addBubble(bubble, isExpanding, + suppressAnimation, /* bubbleToSelect = */ null); + } + + /** Listener of {@link BubbleBarLocation} updates. */ + public interface BubbleBarLocationListener { + + /** Called when {@link BubbleBarLocation} is animated, but change is not yet final. */ + void onBubbleBarLocationAnimated(BubbleBarLocation location); + + /** Called when {@link BubbleBarLocation} is updated permanently. */ + void onBubbleBarLocationUpdated(BubbleBarLocation location); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt index 43e21f4085..680ffca0a0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt @@ -17,10 +17,11 @@ package com.android.launcher3.taskbar.bubbles import android.graphics.Bitmap import android.graphics.Path -import com.android.wm.shell.common.bubbles.BubbleInfo +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage +import com.android.wm.shell.shared.bubbles.BubbleInfo /** An entity in the bubble bar. */ -sealed class BubbleBarItem(open var key: String, open var view: BubbleView) +sealed class BubbleBarItem(open val key: String, open var view: BubbleView) /** Contains state info about a bubble in the bubble bar as well as presentation information. */ data class BubbleBarBubble( @@ -30,8 +31,13 @@ data class BubbleBarBubble( var icon: Bitmap, var dotColor: Int, var dotPath: Path, - var appName: String + var appName: String, + var flyoutMessage: BubbleBarFlyoutMessage?, ) : BubbleBarItem(info.key, view) /** Represents the overflow bubble in the bubble bar. */ -data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view) +data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem(KEY, view) { + companion object { + const val KEY = "Overflow" + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.kt new file mode 100644 index 0000000000..8e176ac6bd --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationCompositeListener.kt @@ -0,0 +1,35 @@ +/* + * 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.launcher3.taskbar.bubbles + +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +/** Composite implementation of [BubbleBarLocationListener] interface */ +class BubbleBarLocationCompositeListener(private val listeners: List) : + BubbleBarLocationListener { + + constructor(vararg listeners: BubbleBarLocationListener) : this(listOf(*listeners)) + + override fun onBubbleBarLocationAnimated(location: BubbleBarLocation?) { + listeners.forEach { it.onBubbleBarLocationAnimated(location) } + } + + override fun onBubbleBarLocationUpdated(location: BubbleBarLocation?) { + listeners.forEach { it.onBubbleBarLocationUpdated(location) } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationDropTarget.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationDropTarget.kt new file mode 100644 index 0000000000..5f80fd56fd --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarLocationDropTarget.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles + +import android.graphics.Rect +import android.view.View +import com.android.launcher3.DropTarget +import com.android.launcher3.dragndrop.DragOptions +import com.android.launcher3.model.data.ItemInfo +import com.android.wm.shell.shared.bubbles.DragZoneFactory +import com.android.wm.shell.shared.bubbles.DropTargetManager + +/** + * Implementation of the {@link DropTarget} that handles drag and drop events over the bubble bar + * locations. + */ +class BubbleBarLocationDropTarget( + private val bubbleBarDropTargetController: BubbleBarDropTargetController, + dragZoneFactory: DragZoneFactory, + private val dropTargetManager: DropTargetManager, + private val isLeftDropTarget: Boolean, +) : DropTarget { + + interface BubbleBarDropTargetController { + + /** Return whether the item info can be dropped on the bubble bar drop target. */ + fun acceptDrop(itemInfo: ItemInfo): Boolean + + /** Called after dragged item info drop on the bubble bar drop target. */ + fun onDrop(itemInfo: ItemInfo, isLeftDropTarget: Boolean) + } + + private val dropRect = dragZoneFactory.getBubbleBarDropRect(isLeftDropTarget) + + override fun isDropEnabled(): Boolean = true + + override fun onDrop(dragObject: DropTarget.DragObject, options: DragOptions) { + bubbleBarDropTargetController.onDrop(dragObject.dragInfo, isLeftDropTarget) + } + + override fun onDragEnter(dragObject: DropTarget.DragObject) { + dropTargetManager.onDragUpdated(dragObject.x, dragObject.y) + } + + override fun onDragOver(dragObject: DropTarget.DragObject) { + dropTargetManager.onDragUpdated(dragObject.x, dragObject.y) + } + + override fun onDragExit(dragObject: DropTarget.DragObject) { + dropTargetManager.onDragUpdated(dragObject.x, dragObject.y) + } + + override fun acceptDrop(dragObject: DropTarget.DragObject): Boolean = + bubbleBarDropTargetController.acceptDrop(dragObject.dragInfo) + + override fun prepareAccessibilityDrop() {} + + override fun getHitRectRelativeToDragLayer(outRect: Rect) { + outRect.set(dropRect) + } + + override fun getDropView(): View? = null +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarParentViewHeightUpdateNotifier.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarParentViewHeightUpdateNotifier.kt new file mode 100644 index 0000000000..f69ad74f80 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarParentViewHeightUpdateNotifier.kt @@ -0,0 +1,24 @@ +/* + * 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.launcher3.taskbar.bubbles + +/** Controls the parent view height. */ +interface BubbleBarParentViewHeightUpdateNotifier { + + /** Notify parent that top boundary should be updated. */ + fun updateTopBoundary() +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt index 9e5ffc9dc4..a34fab2b8f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt @@ -27,8 +27,10 @@ import android.view.View import android.widget.FrameLayout import androidx.core.view.updateLayoutParams import com.android.launcher3.R -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar @@ -41,12 +43,17 @@ class BubbleBarPinController( private lateinit var bubbleBarViewController: BubbleBarViewController private lateinit var bubbleStashController: BubbleStashController + private lateinit var bubbleBarLocationListener: BubbleBarLocationListener private var exclRectWidth: Float = 0f private var exclRectHeight: Float = 0f private var dropTargetView: View? = null - fun init(bubbleControllers: BubbleControllers) { + fun init( + bubbleControllers: BubbleControllers, + bubbleBarLocationListener: BubbleBarLocationListener + ) { + this.bubbleBarLocationListener = bubbleBarLocationListener bubbleBarViewController = bubbleControllers.bubbleBarViewController bubbleStashController = bubbleControllers.bubbleStashController exclRectWidth = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width) @@ -85,6 +92,7 @@ class BubbleBarPinController( val bounds = bubbleBarViewController.bubbleBarBounds val horizontalMargin = bubbleBarViewController.horizontalMargin + bubbleBarLocationListener.onBubbleBarLocationAnimated(location) dropTargetView?.updateLayoutParams { width = bounds.width() height = bounds.height() diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt new file mode 100644 index 0000000000..cdf90dd7c2 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt @@ -0,0 +1,217 @@ +/* + * 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.launcher3.taskbar.bubbles + +import android.animation.ValueAnimator +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnEnd +import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.anim.SpringAnimationBuilder +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarThresholdUtils +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.COLLAPSED +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.EXPANDED +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.STASHED +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.UNKNOWN +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.touch.OverScroll + +/** Handle swipe events on the bubble bar and handle */ +class BubbleBarSwipeController { + + private val context: Context + + private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null + private lateinit var bubbleBarViewController: BubbleBarViewController + private lateinit var bubbleStashController: BubbleStashController + + private var springAnimation: ValueAnimator? = null + private val animatedSwipeTranslation = AnimatedFloat(this::onSwipeUpdate) + + private val unstashThreshold: Int + private val maxOverscroll: Int + + private var swipeState: SwipeState = SwipeState(startState = UNKNOWN) + + constructor(tac: TaskbarActivityContext) : this(tac, DefaultDimensionProvider(tac)) + + @VisibleForTesting + constructor(context: Context, dimensionProvider: DimensionProvider) { + this.context = context + unstashThreshold = dimensionProvider.unstashThreshold + maxOverscroll = dimensionProvider.maxOverscroll + } + + fun init(bubbleControllers: BubbleControllers) { + bubbleStashedHandleViewController = + bubbleControllers.bubbleStashedHandleViewController.orElse(null) + bubbleBarViewController = bubbleControllers.bubbleBarViewController + bubbleStashController = bubbleControllers.bubbleStashController + } + + /** Start tracking a new swipe gesture */ + fun start() { + if (springAnimation != null) reset() + val startState = + when { + bubbleStashController.isStashed -> STASHED + bubbleBarViewController.isExpanded -> EXPANDED + bubbleStashController.isBubbleBarVisible() -> COLLAPSED + else -> UNKNOWN + } + swipeState = SwipeState(startState = startState, currentState = startState) + } + + /** Update swipe distance to [dy] */ + fun swipeTo(dy: Float) { + if (!canHandleSwipe(dy)) { + return + } + animatedSwipeTranslation.updateValue(dy) + + swipeState.passedUnstash = isUnstash(dy) + // Tracking swipe gesture if we pass unstash threshold at least once during gesture + swipeState.isSwipe = swipeState.isSwipe || swipeState.passedUnstash + when { + canUnstash() && swipeState.passedUnstash -> { + swipeState.currentState = COLLAPSED + bubbleStashController.showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + } + canStash() && !swipeState.passedUnstash -> { + swipeState.currentState = STASHED + bubbleStashController.stashBubbleBar() + } + } + } + + /** Finish tracking swipe gesture. Animate views back to resting state */ + fun finish() { + if (swipeState.passedUnstash && swipeState.startState in setOf(STASHED, COLLAPSED)) { + bubbleStashController.showBubbleBar(expandBubbles = true, bubbleBarGesture = true) + } + if (animatedSwipeTranslation.value == 0f) { + reset() + } else { + springToRest() + } + } + + /** Returns `true` if we are tracking a swipe gesture */ + fun isSwipeGesture(): Boolean { + return swipeState.isSwipe + } + + private fun canHandleSwipe(dy: Float): Boolean { + return when (swipeState.startState) { + STASHED -> { + if (swipeState.currentState == COLLAPSED) { + // if we have unstashed the bar, allow swipe in both directions + true + } else { + // otherwise, only allow swipe up on stash handle + dy < 0 + } + } + COLLAPSED -> dy < 0 // collapsed bar can only be swiped up + UNKNOWN, + EXPANDED -> false // expanded bar can't be swiped + } + } + + private fun isUnstash(dy: Float): Boolean { + return dy < -unstashThreshold + } + + private fun canStash(): Boolean { + // Only allow stashing if we started from stashed state + return swipeState.startState == STASHED && swipeState.currentState == COLLAPSED + } + + private fun canUnstash(): Boolean { + return swipeState.currentState == STASHED + } + + private fun reset() { + springAnimation?.let { + if (it.isRunning) { + it.removeAllListeners() + it.cancel() + animatedSwipeTranslation.updateValue(0f) + } + } + springAnimation = null + swipeState = SwipeState(startState = UNKNOWN) + } + + private fun onSwipeUpdate(value: Float) { + val dampedSwipe = -OverScroll.dampedScroll(-value, maxOverscroll).toFloat() + bubbleStashedHandleViewController?.setTranslationYForSwipe(dampedSwipe) + bubbleBarViewController.setTranslationYForSwipe(dampedSwipe) + } + + private fun springToRest() { + springAnimation = + SpringAnimationBuilder(context) + .setStartValue(animatedSwipeTranslation.value) + .setEndValue(0f) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + .build(animatedSwipeTranslation, AnimatedFloat.VALUE) + .also { it.doOnEnd { reset() } } + springAnimation?.start() + } + + internal data class SwipeState( + val startState: BarState, + var currentState: BarState = UNKNOWN, + var passedUnstash: Boolean = false, + var isSwipe: Boolean = false, + ) + + internal enum class BarState { + UNKNOWN, + STASHED, + COLLAPSED, + EXPANDED, + } + + /** Allows overriding the dimension provider for testing */ + @VisibleForTesting + interface DimensionProvider { + val unstashThreshold: Int + val maxOverscroll: Int + } + + private class DefaultDimensionProvider(taskbarActivityContext: TaskbarActivityContext) : + DimensionProvider { + override val unstashThreshold: Int + override val maxOverscroll: Int + + init { + val resources = taskbarActivityContext.resources + unstashThreshold = + TaskbarThresholdUtils.getFromNavThreshold( + resources, + taskbarActivityContext.deviceProfile, + ) + maxOverscroll = + taskbarActivityContext.deviceProfile.deviceProperties.heightPx - unstashThreshold + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index ca0cb94bad..adfe59a93f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -15,13 +15,10 @@ */ package com.android.launcher3.taskbar.bubbles; -import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.NonNull; @@ -30,6 +27,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.LayoutDirection; @@ -38,108 +36,83 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; -import androidx.dynamicanimation.animation.SpringForce; - +import com.android.app.animation.Interpolators; import com.android.launcher3.R; -import com.android.launcher3.anim.SpringAnimationBuilder; -import com.android.launcher3.util.DisplayController; -import com.android.wm.shell.Flags; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.launcher3.anim.AnimatorListeners; +import com.android.launcher3.taskbar.BarsLocationAnimatorHelper; +import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; /** - * The view that holds all the bubble views. Modifying this view should happen - * through - * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, - * removes, updates, - * selection) should happen through {@link BubbleBarController} which is the - * source of truth + * The view that holds all the bubble views. Modifying this view should happen through + * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates, + * selection) should happen through {@link BubbleBarController} which is the source of truth * for state information about the bubbles. *

* The bubble bar has a couple of visual states: * - stashed as a handle - * - unstashed but collapsed, in this state the bar is showing but the bubbles - * are stacked within it - * - unstashed and expanded, in this state the bar is showing and the bubbles - * are shown in a row - * with one of the bubbles being selected. Additionally, WMShell will display - * the expanded bubble + * - unstashed but collapsed, in this state the bar is showing but the bubbles are stacked within it + * - unstashed and expanded, in this state the bar is showing and the bubbles are shown in a row + * with one of the bubbles being selected. Additionally, WMShell will display the expanded bubble * view above the bar. *

* The bubble bar has some behavior related to taskbar: - * - When taskbar is unstashed, bubble bar will also become unstashed (but in - * its "collapsed" + * - When taskbar is unstashed, bubble bar will also become unstashed (but in its "collapsed" * state) - * - When taskbar is stashed, bubble bar will also become stashed (unless bubble - * bar is in its + * - When taskbar is stashed, bubble bar will also become stashed (unless bubble bar is in its * "expanded" state) * - When bubble bar is in its "expanded" state, taskbar becomes stashed *

- * If there are no bubbles, the bubble bar and bubble stashed handle are not - * shown. Additionally + * If there are no bubbles, the bubble bar and bubble stashed handle are not shown. Additionally * the bubble bar and stashed handle are not shown on lockscreen. *

- * When taskbar is in persistent or 3 button nav mode, the bubble bar is not - * available, and instead + * When taskbar is in persistent or 3 button nav mode, the bubble bar is not available, and instead * the bubbles are shown fully by WMShell in their floating mode. */ public class BubbleBarView extends FrameLayout { + public static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L; + public static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L; + public static final long FADE_OUT_BUBBLE_BAR_DURATION_MS = 150L; private static final String TAG = "BubbleBarView"; - - // TODO: (b/273594744) calculate the amount of space we have and base the max on - // that - // if it's smaller than 5. + // TODO: (b/273594744) calculate the amount of space we have and base the max on that + // if it's smaller than 5. private static final int MAX_BUBBLES = 5; private static final int MAX_VISIBLE_BUBBLES_COLLAPSED = 2; private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200; - private static final int WIDTH_ANIMATION_DURATION_MS = 200; + private static final int WIDTH_ANIMATION_DURATION_MS = 400; private static final int SCALE_ANIMATION_DURATION_MS = 200; - private static final long FADE_OUT_ANIM_ALPHA_DURATION_MS = 50L; - private static final long FADE_OUT_ANIM_ALPHA_DELAY_MS = 50L; - private static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L; - // During fade out animation we shift the bubble bar 1/80th of the screen width - private static final float FADE_OUT_ANIM_POSITION_SHIFT = 1 / 80f; - - private static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L; - // Use STIFFNESS_MEDIUMLOW which is not defined in the API constants - private static final float FADE_IN_ANIM_POSITION_SPRING_STIFFNESS = 400f; - // During fade in animation we shift the bubble bar 1/60th of the screen width - private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f; - - private static final int SCALE_IN_ANIMATION_DURATION_MS = 250; - /** - * Custom property to set alpha value for the bar view while a bubble is being - * dragged. + * Custom property to set alpha value for the bar view while a bubble is being dragged. * Skips applying alpha to the dragged bubble. */ - private static final FloatProperty BUBBLE_DRAG_ALPHA = new FloatProperty<>("bubbleDragAlpha") { - @Override - public void setValue(BubbleBarView bubbleBarView, float alpha) { - bubbleBarView.setAlphaDuringBubbleDrag(alpha); - } + private static final FloatProperty BUBBLE_DRAG_ALPHA = + new FloatProperty<>("bubbleDragAlpha") { + @Override + public void setValue(BubbleBarView bubbleBarView, float alpha) { + bubbleBarView.setAlphaDuringBubbleDrag(alpha); + } - @Override - public Float get(BubbleBarView bubbleBarView) { - return bubbleBarView.mAlphaDuringDrag; - } - }; + @Override + public Float get(BubbleBarView bubbleBarView) { + return bubbleBarView.mAlphaDuringDrag; + } + }; private final BubbleBarBackground mBubbleBarBackground; - private boolean mIsAnimatingNewBubble = false; - /** - * The current bounds of all the bubble bar. Note that these bounds may not - * account for - * translation. The bounds should be retrieved using - * {@link #getBubbleBarBounds()} which + * The current bounds of all the bubble bar. Note that these bounds may not account for + * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which * updates the bounds and accounts for translation. */ private final Rect mBubbleBarBounds = new Rect(); @@ -170,26 +143,25 @@ public class BubbleBarView extends FrameLayout { private float mRelativePivotX = 1f; private float mRelativePivotY = 1f; - // An animator that represents the expansion state of the bubble bar, where 0 - // corresponds to the + // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the // collapsed state and 1 to the fully expanded state. - private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1); - - /** - * An animator used for scaling in a new bubble to the bubble bar while - * expanded. - */ @Nullable - private ValueAnimator mNewBubbleScaleInAnimator = null; + private ValueAnimator mWidthAnimator; + + @Nullable + private ValueAnimator mDismissAnimator = null; + + /** An animator used for animating individual bubbles in the bubble bar while expanded. */ + @Nullable + private BubbleAnimator mBubbleAnimator = null; @Nullable private ValueAnimator mScalePaddingAnimator; + @Nullable private Animator mBubbleBarLocationAnimator = null; - // We don't reorder the bubbles when they are expanded as it could be jarring - // for the user - // this runnable will be populated with any reordering of the bubbles that - // should be applied + // We don't reorder the bubbles when they are expanded as it could be jarring for the user + // this runnable will be populated with any reordering of the bubbles that should be applied // once they are collapsed. @Nullable private Runnable mReorderRunnable; @@ -201,8 +173,13 @@ public class BubbleBarView extends FrameLayout { @Nullable private BubbleView mDraggedBubbleView; + @Nullable + private BubbleView mDismissedByDragBubbleView; private float mAlphaDuringDrag = 1f; + /** Additional translation in the y direction that is applied to each bubble */ + private float mBubbleOffsetY; + private Controller mController; private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED; @@ -221,7 +198,6 @@ public class BubbleBarView extends FrameLayout { public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setAlpha(0); setVisibility(INVISIBLE); mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap); mBubbleBarPadding = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); @@ -229,7 +205,7 @@ public class BubbleBarView extends FrameLayout { mExpandedBarIconsSpacing = getResources().getDimensionPixelSize( R.dimen.bubblebar_expanded_icon_spacing); mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation); - mDragElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_drag_elevation); + mDragElevation = getResources().getDimensionPixelSize(R.dimen.dragged_bubble_elevation); mPointerSize = getResources() .getDimensionPixelSize(R.dimen.bubblebar_pointer_visible_size); @@ -237,33 +213,9 @@ public class BubbleBarView extends FrameLayout { mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarExpandedHeight()); setBackgroundDrawable(mBubbleBarBackground); - - mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS); - - addAnimationCallBacks(mWidthAnimator, - /* onStart= */ () -> mBubbleBarBackground.showArrow(true), - /* onEnd= */ () -> { - mBubbleBarBackground.showArrow(mIsBarExpanded); - if (!mIsBarExpanded && mReorderRunnable != null) { - mReorderRunnable.run(); - mReorderRunnable = null; - } - // If the bar was just collapsed and the overflow was the last bubble that was - // selected, set the first bubble as selected. - if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null - && mSelectedBubbleView != null - && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) { - BubbleView firstBubble = (BubbleView) getChildAt(0); - mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey()); - } - updateWidth(); - }, - /* onUpdate= */ animator -> { - updateBubblesLayoutProperties(mBubbleBarLocation); - invalidate(); - }); } + /** * Animates icon sizes and spacing between icons and bubble bar borders. * @@ -274,9 +226,6 @@ public class BubbleBarView extends FrameLayout { if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) { return; } - if (!Flags.animateBubbleSizeChange()) { - setIconSizeAndPadding(newIconSize, newBubbleBarPadding); - } if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) { mScalePaddingAnimator.cancel(); } @@ -294,7 +243,8 @@ public class BubbleBarView extends FrameLayout { /* onUpdate= */ animator -> { float transitionProgress = (float) animator.getAnimatedValue(); if (isIconSizeUpdated) { - mIconScale = initialScale + (targetScale - initialScale) * transitionProgress; + mIconScale = + initialScale + (targetScale - initialScale) * transitionProgress; } if (isPaddingUpdated) { mBubbleBarPadding = initialPadding @@ -311,8 +261,7 @@ public class BubbleBarView extends FrameLayout { public void setTranslationX(float translationX) { super.setTranslationX(translationX); if (mDraggedBubbleView != null) { - // Apply reverse of the translation as an offset to the dragged view. This - // ensures + // Apply reverse of the translation as an offset to the dragged view. This ensures // that the dragged bubble stays at the current location on the screen and its // position is not affected by the parent translation. mDraggedBubbleView.setOffsetX(-translationX); @@ -320,12 +269,61 @@ public class BubbleBarView extends FrameLayout { } /** - * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar - * borders. + * Set scale for bubble bar background in x direction + */ + public void setBackgroundScaleX(float scaleX) { + mBubbleBarBackground.setScaleX(scaleX); + } + + /** + * Set scale for bubble bar background in y direction + */ + public void setBackgroundScaleY(float scaleY) { + mBubbleBarBackground.setScaleY(scaleY); + } + + /** + * Set alpha for bubble views + */ + public void setBubbleAlpha(float alpha) { + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setAlpha(alpha); + } + } + + /** + * Set alpha for bar background + */ + public void setBackgroundAlpha(float alpha) { + mBubbleBarBackground.setAlpha((int) (255 * alpha)); + } + + /** + * Sets offset of each bubble view in the y direction from the base position in the bar. + */ + public void setBubbleOffsetY(float offsetY) { + mBubbleOffsetY = offsetY; + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setTranslationY(getBubbleTranslationY()); + } + } + + /** + * Set the bubble icons size and spacing between the bubbles and the borders of the bubble + * bar. + */ + public void setIconSizeAndPaddingForPinning(float newIconSize, float newBubbleBarPadding) { + mBubbleBarPadding = newBubbleBarPadding; + mIconScale = newIconSize / mIconSize; + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + + /** + * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders. * * @param newIconSize new icon size - * @param newBubbleBarPadding newBubbleBarPadding between icons and bubble bar - * borders. + * @param newBubbleBarPadding newBubbleBarPadding between icons and bubble bar borders. */ public void setIconSizeAndPadding(float newIconSize, float newBubbleBarPadding) { // TODO(b/335457839): handle new bubble animation during the size change @@ -338,7 +336,7 @@ public class BubbleBarView extends FrameLayout { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); - childView.setScaleY(mIconScale); + childView.setScaleX(mIconScale); childView.setScaleY(mIconScale); FrameLayout.LayoutParams params = (LayoutParams) childView.getLayoutParams(); params.height = (int) mIconSize; @@ -362,8 +360,7 @@ public class BubbleBarView extends FrameLayout { mBubbleBarBounds.bottom = bottom; // The bubble bar handle is aligned according to the relative pivot, - // by default it's aligned to the bottom edge of the screen so scale towards - // that + // by default it's aligned to the bottom edge of the screen so scale towards that setPivotX(mRelativePivotX * getWidth()); setPivotY(mRelativePivotY * getHeight()); @@ -384,6 +381,48 @@ public class BubbleBarView extends FrameLayout { } } + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); + // Always show only expand action as the menu is only for collapsed bubble bar + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all, + getResources().getString(R.string.bubble_bar_action_dismiss_all))); + if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right, + getResources().getString(R.string.bubble_bar_action_move_right))); + } else { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left, + getResources().getString(R.string.bubble_bar_action_move_left))); + } + } + + @Override + public boolean performAccessibilityActionInternal(int action, + @androidx.annotation.Nullable Bundle arguments) { + if (action == AccessibilityNodeInfo.ACTION_EXPAND + || action == AccessibilityNodeInfo.ACTION_CLICK) { + mController.expandBubbleBar(); + return true; + } + if (action == R.id.action_dismiss_all) { + mController.dismissBubbleBar(); + return true; + } + if (action == R.id.action_move_left) { + mController.updateBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR); + return true; + } + if (action == R.id.action_move_right) { + mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR); + return true; + } + return super.performAccessibilityActionInternal(action, arguments); + } + @SuppressLint("RtlHardcoded") private void onBubbleBarLocationChanged() { final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); @@ -392,6 +431,7 @@ public class BubbleBarView extends FrameLayout { LayoutParams lp = (LayoutParams) getLayoutParams(); lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT); setLayoutParams(lp); // triggers a relayout + updateBubbleAccessibilityStates(); } /** @@ -420,37 +460,30 @@ public class BubbleBarView extends FrameLayout { return; } mDragging = dragging; - setElevation(dragging ? mDragElevation : mBubbleElevation); + mController.setIsDragging(dragging); if (!mDragging) { - // Relayout after dragging to ensure that the dragged bubble is positioned - // correctly + // Relayout after dragging to ensure that the dragged bubble is positioned correctly requestLayout(); } } /** - * Get translation for bubble bar when drag is released and it needs to animate - * back to the + * Get translation for bubble bar when drag is released and it needs to animate back to the * resting position. - * Resting position is based on the supplied location. If the supplied location - * is different - * from the internal location that was used during bubble bar layout, - * translation values are + * Resting position is based on the supplied location. If the supplied location is different + * from the internal location that was used during bubble bar layout, translation values are * calculated to position the bar at the desired location. * * @param initialTranslation initial bubble bar translation at the start of drag - * @param location desired location of the bubble bar when drag is - * released + * @param location desired location of the bubble bar when drag is released * @return point with x and y values representing translation on x and y-axis */ public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation, BubbleBarLocation location) { float dragEndTranslationX = initialTranslation.x; if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != location.isOnLeft(isLayoutRtl())) { - // Bubble bar is laid out on left or right side of the screen. And the desired - // new - // location is on the other side. Calculate x translation value required to - // shift + // Bubble bar is laid out on left or right side of the screen. And the desired new + // location is on the other side. Calculate x translation value required to shift // bubble bar from one side to the other. final float shift = getDistanceFromOtherSide(); if (location.isOnLeft(isLayoutRtl())) { @@ -467,19 +500,14 @@ public class BubbleBarView extends FrameLayout { } /** - * Get translation for a bubble when drag is released and it needs to animate - * back to the + * Get translation for a bubble when drag is released and it needs to animate back to the * resting position. - * Resting position is based on the supplied location. If the supplied location - * is different - * from the internal location that was used during bubble bar layout, - * translation values are + * Resting position is based on the supplied location. If the supplied location is different + * from the internal location that was used during bubble bar layout, translation values are * calculated to position the bar at the desired location. * - * @param initialTranslation initial bubble translation inside the bar at the - * start of drag - * @param location desired location of the bubble bar when drag is - * released + * @param initialTranslation initial bubble translation inside the bar at the start of drag + * @param location desired location of the bubble bar when drag is released * @return point with x and y values representing translation on x and y-axis */ public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation, @@ -489,8 +517,9 @@ public class BubbleBarView extends FrameLayout { if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != newLocationOnLeft) { // Calculate translationX based on bar and bubble translations float bubbleBarTx = getBubbleBarDragReleaseTranslation(initialTranslation, location).x; - float bubbleTx = getExpandedBubbleTranslationX( - indexOfChild(mDraggedBubbleView), getChildCount(), newLocationOnLeft); + float bubbleTx = + getExpandedBubbleTranslationX( + indexOfChild(mDraggedBubbleView), getChildCount(), newLocationOnLeft); dragEndTranslationX = bubbleBarTx + bubbleTx; } // translationY does not change during drag and can be reused @@ -508,9 +537,18 @@ public class BubbleBarView extends FrameLayout { return (float) (displayWidth - getWidth() - margin); } + /** Set whether the background should show the drop target */ + public void showDropTarget(boolean isDropTarget) { + mBubbleBarBackground.showDropTarget(isDropTarget); + } + + /** Returns whether the Bubble Bar is currently displaying a drop target. */ + public boolean isShowingDropTarget() { + return mBubbleBarBackground.isShowingDropTarget(); + } + /** - * Animate bubble bar to the given location transiently. Does not modify the - * layout or the value + * Animate bubble bar to the given location transiently. Does not modify the layout or the value * returned by {@link #getBubbleBarLocation()}. */ public void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { @@ -521,84 +559,58 @@ public class BubbleBarView extends FrameLayout { // Location animation uses two separate animators. // First animator hides the bar. - // After it completes, bubble positions in the bar and arrow position is - // updated. + // After it completes, bubble positions in the bar and arrow position is updated. // Second animator is started to show the bar. - mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator(bubbleBarLocation); - mBubbleBarLocationAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - updateBubblesLayoutProperties(bubbleBarLocation); - mBubbleBarBackground.setAnchorLeft(bubbleBarLocation.isOnLeft(isLayoutRtl())); - - // Animate it in - mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator(bubbleBarLocation); - mBubbleBarLocationAnimator.start(); - } - }); + mBubbleBarLocationAnimator = animateToBubbleBarLocationOut(bubbleBarLocation); + mBubbleBarLocationAnimator.addListener(AnimatorListeners.forEndCallback(() -> { + // Animate it in + mBubbleBarLocationAnimator = animateToBubbleBarLocationIn(mBubbleBarLocation, + bubbleBarLocation); + mBubbleBarLocationAnimator.start(); + })); mBubbleBarLocationAnimator.start(); } - private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation newLocation) { - final float shift = getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT; - final boolean onLeft = newLocation.isOnLeft(isLayoutRtl()); - final float tx = getTranslationX() + (onLeft ? -shift : shift); - - ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, VIEW_TRANSLATE_X, tx) - .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS); - positionAnim.setInterpolator(EMPHASIZED_ACCELERATE); - - ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 0f) - .setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS); - alphaAnim.setStartDelay(FADE_OUT_ANIM_ALPHA_DELAY_MS); - - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(positionAnim, alphaAnim); - return animatorSet; + /** Creates animator for animating bubble bar in. */ + public Animator animateToBubbleBarLocationIn(BubbleBarLocation fromLocation, + BubbleBarLocation toLocation) { + updateBubblesLayoutProperties(toLocation); + mBubbleBarBackground.setAnchorLeft(toLocation.isOnLeft(isLayoutRtl())); + ObjectAnimator alphaInAnim = ObjectAnimator.ofFloat(BubbleBarView.this, + getLocationAnimAlphaProperty(), 1f); + return BarsLocationAnimatorHelper.getBubbleBarLocationInAnimator(toLocation, fromLocation, + getDistanceFromOtherSide(), alphaInAnim, this); } - private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation newLocation) { - final float shift = getResources().getDisplayMetrics().widthPixels * FADE_IN_ANIM_POSITION_SHIFT; - - final boolean onLeft = newLocation.isOnLeft(isLayoutRtl()); - final float startTx; - final float finalTx; - if (newLocation == mBubbleBarLocation) { - // Animated location matches layout location. - finalTx = 0; - } else { - // We are animating in to a transient location, need to move the bar - // accordingly. - finalTx = getDistanceFromOtherSide() * (onLeft ? -1 : 1); - } - if (onLeft) { - // Bar will be shown on the left side. Start point is shifted right. - startTx = finalTx + shift; - } else { - // Bar will be shown on the right side. Start point is shifted left. - startTx = finalTx - shift; - } - - ValueAnimator positionAnim = new SpringAnimationBuilder(getContext()) - .setStartValue(startTx) - .setEndValue(finalTx) - .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) - .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS) - .build(this, VIEW_TRANSLATE_X); - - ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 1f) - .setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS); - - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(positionAnim, alphaAnim); - return animatorSet; + /** + * Creates animator for animating bubble bar out. + * + * @param targetLocation the location bubble br should animate to. + */ + public Animator animateToBubbleBarLocationOut(BubbleBarLocation targetLocation) { + ObjectAnimator alphaOutAnim = ObjectAnimator.ofFloat( + this, getLocationAnimAlphaProperty(), 0f); + Animator outAnimation = BarsLocationAnimatorHelper.getBubbleBarLocationOutAnimator( + this, + targetLocation, + alphaOutAnim); + outAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + // need to restore the original bar view state in case icon is dropped to the bubble + // bar original location + updateBubblesLayoutProperties(targetLocation); + mBubbleBarBackground.setAnchorLeft(targetLocation.isOnLeft(isLayoutRtl())); + setTranslationX(0f); + } + }); + return outAnimation; } /** * Get property that can be used to animate the alpha value for the bar. * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_ALPHA}. - * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_ALPHA} - * otherwise. + * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_ALPHA} otherwise. */ private FloatProperty getLocationAnimAlphaProperty() { return mDraggedBubbleView == null ? VIEW_ALPHA : BUBBLE_DRAG_ALPHA; @@ -606,14 +618,11 @@ public class BubbleBarView extends FrameLayout { /** * Set alpha value for the bar while a bubble is being dragged. - * We can not update the alpha on the bar directly because the dragged bubble - * would be affected + * We can not update the alpha on the bar directly because the dragged bubble would be affected * as well. As it is a child view. - * Instead, while a bubble is being dragged, set alpha on each child view, that - * is not the + * Instead, while a bubble is being dragged, set alpha on each child view, that is not the * dragged view. And set an alpha on the background. - * This allows for the dragged bubble to remain visible while the bar is hidden - * during + * This allows for the dragged bubble to remain visible while the bar is hidden during * animation. */ private void setAlphaDuringBubbleDrag(float alpha) { @@ -638,33 +647,47 @@ public class BubbleBarView extends FrameLayout { } setAlphaDuringBubbleDrag(1f); setTranslationX(0f); - setAlpha(1f); + if (mIsBarExpanded && getBubbleChildCount() > 0) { + setAlpha(1f); + } } - /** - * Get bubble bar top coordinate on screen when bar is resting - */ - public int getRestingTopPositionOnScreen() { - int displayHeight = DisplayController.INSTANCE.get(getContext()).getInfo().currentSize.y; + /** Get the distance between the bubble bar top coordinate and the bottom of the screen */ + public int getTopToScreenBottom() { + // the bottom of the bubble bar is aligned with the bottom of the screen. the distance + // between the top of the bubble bar and the bottom of the screen is the height of the + // bubble bar minus the y translation. since the bubble bar is always above the bottom of + // the screen, the translation is negative and the overall result is a positive value that + // represents the distance int bubbleBarHeight = getBubbleBarBounds().height(); - return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY(); + return bubbleBarHeight - (int) mController.getBubbleBarTranslationY(); } - /** - * Updates the bounds with translation that may have been applied and returns - * the result. - */ + /** Returns the bounds with translation that may have been applied. */ public Rect getBubbleBarBounds() { - mBubbleBarBounds.top = getTop() + (int) getTranslationY() + mPointerSize; - mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY(); - return mBubbleBarBounds; + Rect bounds = new Rect(mBubbleBarBounds); + bounds.top = getTop() + (int) getTranslationY() + mPointerSize; + bounds.bottom = getBottom() + (int) getTranslationY(); + return bounds; + } + + /** Returns the expanded bounds with translation that may have been applied. */ + public Rect getBubbleBarExpandedBounds() { + Rect expandedBounds = getBubbleBarBounds(); + if (!isExpanded() || isExpanding()) { + if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { + expandedBounds.right = expandedBounds.left + (int) expandedWidth(); + } else { + expandedBounds.left = expandedBounds.right - (int) expandedWidth(); + } + } + return expandedBounds; } /** - * Set bubble bar relative pivot value for X and Y, applied as a fraction of - * view width/height + * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height * respectively. If the value is not in range of 0 to 1 it will be normalized. - * + * * @param x relative X pivot value in range 0..1 * @param y relative Y pivot value in range 0..1 */ @@ -693,68 +716,190 @@ public class BubbleBarView extends FrameLayout { return mRelativePivotY; } - /** Notifies the bubble bar that a new bubble animation is starting. */ - public void onAnimatingBubbleStarted() { - mIsAnimatingNewBubble = true; + /** Add a new bubble to the bubble bar without updating the selected bubble. */ + public void addBubble(BubbleView bubble, boolean suppressAnimation) { + addBubble(bubble, /* bubbleToSelect = */ null, suppressAnimation); } - /** Notifies the bubble bar that a new bubble animation is complete. */ - public void onAnimatingBubbleCompleted() { - mIsAnimatingNewBubble = false; - } + /** + * Add a new bubble to the bubble bar and selects the provided bubble. + * + * @param bubble bubble to add + * @param bubbleToSelect if {@code null}, then selected bubble does not change + */ + public void addBubble(BubbleView bubble, @Nullable BubbleView bubbleToSelect, + boolean suppressAnimation) { + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, + Gravity.LEFT); + final int index = bubble.isOverflow() ? getChildCount() : 0; - /** Add a new bubble to the bubble bar. */ - public void addBubble(View bubble, FrameLayout.LayoutParams lp) { - if (isExpanded()) { + if (isExpanded() && !suppressAnimation) { // if we're expanded scale the new bubble in bubble.setScaleX(0f); bubble.setScaleY(0f); - addView(bubble, 0, lp); - createNewBubbleScaleInAnimator(bubble); - mNewBubbleScaleInAnimator.start(); + addView(bubble, index, lp); + bubble.showDotIfNeeded(/* animate= */ false); + + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + + @Override + public void onAnimationEnd() { + updateLayoutParams(); + mBubbleAnimator = null; + } + + @Override + public void onAnimationCancel() { + bubble.setScaleX(1); + bubble.setScaleY(1); + } + + @Override + public void onAnimationUpdate(float animatedFraction) { + bubble.setScaleX(animatedFraction); + bubble.setScaleY(animatedFraction); + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + if (bubbleToSelect != null) { + mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), + indexOfChild(bubbleToSelect), listener); + } else { + mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener); + } } else { - addView(bubble, 0, lp); + addView(bubble, index, lp); } } - private void createNewBubbleScaleInAnimator(View bubble) { - mNewBubbleScaleInAnimator = ValueAnimator.ofFloat(0, 1); - mNewBubbleScaleInAnimator.setDuration(SCALE_IN_ANIMATION_DURATION_MS); - mNewBubbleScaleInAnimator.addUpdateListener(animation -> { - float animatedFraction = animation.getAnimatedFraction(); - bubble.setScaleX(animatedFraction); - bubble.setScaleY(animatedFraction); - updateBubblesLayoutProperties(mBubbleBarLocation); - invalidate(); - }); - mNewBubbleScaleInAnimator.addListener(new AnimatorListenerAdapter() { + /** Add a new bubble and remove an old bubble from the bubble bar. */ + public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble, + @Nullable BubbleView bubbleToSelect, Runnable onEndRunnable) { + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, + Gravity.LEFT); + int addedIndex = addedBubble.isOverflow() ? getChildCount() : 0; + if (!isExpanded()) { + removeView(removedBubble); + addView(addedBubble, addedIndex, lp); + if (onEndRunnable != null) { + onEndRunnable.run(); + } + return; + } + addedBubble.setScaleX(0f); + addedBubble.setScaleY(0f); + addView(addedBubble, addedIndex, lp); + int indexOfCurrentSelectedBubble = indexOfChild(mSelectedBubbleView); + int indexOfBubbleToRemove = indexOfChild(removedBubble); + int indexOfNewlySelectedBubble = bubbleToSelect == null + ? indexOfCurrentSelectedBubble : indexOfChild(bubbleToSelect); + // Since removed bubble is kept till the end of the animation we should check if there are + // more than one bubble. In such a case the bar will remain open without the selected bubble + if (mSelectedBubbleView == removedBubble + && bubbleToSelect == null + && getBubbleChildCount() > 1) { + Log.w(TAG, "Remove the currently selected bubble without selecting a new one."); + } + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + @Override - public void onAnimationCancel(Animator animation) { - bubble.setScaleX(1); - bubble.setScaleY(1); + public void onAnimationEnd() { + removeView(removedBubble); + updateLayoutParams(); + mBubbleAnimator = null; + if (onEndRunnable != null) { + onEndRunnable.run(); + } } @Override - public void onAnimationEnd(Animator animation) { - updateWidth(); - mNewBubbleScaleInAnimator = null; + public void onAnimationCancel() { + addedBubble.setScaleX(1); + addedBubble.setScaleY(1); + removedBubble.setScaleX(0); + removedBubble.setScaleY(0); } - }); + + @Override + public void onAnimationUpdate(float animatedFraction) { + addedBubble.setScaleX(animatedFraction); + addedBubble.setScaleY(animatedFraction); + removedBubble.setScaleX(1 - animatedFraction); + removedBubble.setScaleY(1 - animatedFraction); + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + mBubbleAnimator.animateNewAndRemoveOld(indexOfCurrentSelectedBubble, + indexOfNewlySelectedBubble, indexOfBubbleToRemove, addedIndex, listener); } - // TODO: (b/280605790) animate it @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { - if (getChildCount() + 1 > MAX_BUBBLES) { - // the last child view is the overflow bubble and we shouldn't remove that. - // remove the - // second to last child view. - removeViewInLayout(getChildAt(getChildCount() - 2)); - } super.addView(child, index, params); - updateWidth(); + updateLayoutParams(); updateBubbleAccessibilityStates(); updateContentDescription(); + updateDotsAndBadgesIfCollapsed(); + } + + /** Removes the given bubble from the bubble bar. */ + public void removeBubble(View bubble) { + if (isExpanded()) { + final boolean dismissedByDrag = mDraggedBubbleView == bubble; + if (dismissedByDrag) { + mDismissedByDragBubbleView = mDraggedBubbleView; + } + boolean removingLastRemainingBubble = getBubbleChildCount() == 1; + int bubbleCount = getChildCount(); + mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, + bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl())); + BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { + + @Override + public void onAnimationEnd() { + removeView(bubble); + mBubbleAnimator = null; + } + + @Override + public void onAnimationCancel() { + bubble.setScaleX(0); + bubble.setScaleY(0); + } + + @Override + public void onAnimationUpdate(float animatedFraction) { + // don't update the scale if this bubble was dismissed by drag + if (!dismissedByDrag) { + bubble.setScaleX(1 - animatedFraction); + bubble.setScaleY(1 - animatedFraction); + } + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + } + }; + int bubbleIndex = indexOfChild(bubble); + BubbleView lastBubble = (BubbleView) getChildAt(bubbleCount - 1); + String lastBubbleKey = lastBubble.getBubble().getKey(); + boolean removingLastBubble = + BubbleBarOverflow.KEY.equals(lastBubbleKey) + ? bubbleIndex == bubbleCount - 2 + : bubbleIndex == bubbleCount - 1; + mBubbleAnimator.animateRemovedBubble( + indexOfChild(bubble), indexOfChild(mSelectedBubbleView), removingLastBubble, + removingLastRemainingBubble, listener); + if (removingLastRemainingBubble && mDismissAnimator == null) { + createDismissAnimator().start(); + } + } else { + removeView(bubble); + } } // TODO: (b/283309949) animate it @@ -765,15 +910,82 @@ public class BubbleBarView extends FrameLayout { mSelectedBubbleView = null; mBubbleBarBackground.showArrow(false); } - updateWidth(); + updateLayoutParams(); updateBubbleAccessibilityStates(); updateContentDescription(); + mDismissedByDragBubbleView = null; + updateDotsAndBadgesIfCollapsed(); } - private void updateWidth() { - LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); - lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth()); - setLayoutParams(lp); + private ValueAnimator createDismissAnimator() { + ValueAnimator animator = + ValueAnimator.ofFloat(0, 1).setDuration(FADE_OUT_BUBBLE_BAR_DURATION_MS); + animator.setInterpolator(Interpolators.EMPHASIZED); + Runnable onEnd = () -> { + mDismissAnimator = null; + setAlpha(0); + }; + addAnimationCallBacks(animator, /* onStart= */ null, onEnd, + /* onUpdate= */ anim -> setAlpha(1 - anim.getAnimatedFraction())); + mDismissAnimator = animator; + return animator; + } + + /** Dismisses the bubble bar */ + public void dismiss(Runnable onDismissed) { + if (mDismissAnimator == null) { + createDismissAnimator().start(); + } + addAnimationCallBacks(mDismissAnimator, null, onDismissed, null); + } + + /** + * Return child views in the order which they are shown on the screen. + *

+ * Child views (bubbles) are always ordered based on recency. The most recent bubble is at index + * 0. + * For example if the child views are (1)(2)(3) then (1) is the most recent bubble and at index + * 0.
+ * + * How bubbles show up on the screen depends on the bubble bar location. If the bar is on the + * left, the most recent bubble is shown on the right. The bubbles from the example above would + * be shown as: (3)(2)(1).
+ * + * If bubble bar is on the right, then the most recent bubble is on the left. Bubbles from the + * example above would be shown as: (1)(2)(3). + */ + private List getChildViewsInOnScreenOrder() { + List childViews = new ArrayList<>(getChildCount()); + for (int i = 0; i < getChildCount(); i++) { + childViews.add(getChildAt(i)); + } + if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { + // Visually child views are shown in reverse order when bar is on the left + return childViews.reversed(); + } + return childViews; + } + + private void updateDotsAndBadgesIfCollapsed() { + if (isExpanded()) { + return; + } + for (int i = 0; i < getChildCount(); i++) { + BubbleView bubbleView = (BubbleView) getChildAt(i); + // when we're collapsed, the first bubble should show the badge and the dot if it has + // it. the rest of the bubbles should hide their badges and dots. + if (i == 0) { + bubbleView.showBadge(); + if (bubbleView.hasUnseenContent()) { + bubbleView.showDotIfNeeded(/* animate= */ true); + } else { + bubbleView.hideDot(); + } + } else { + bubbleView.hideBadge(); + bubbleView.hideDot(); + } + } } private void updateLayoutParams() { @@ -788,38 +1000,34 @@ public class BubbleBarView extends FrameLayout { : getBubbleBarCollapsedHeight(); } - /** - * @return the horizontal margin between the bubble bar and the edge of the - * screen. - */ + /** @return the horizontal margin between the bubble bar and the edge of the screen. */ int getHorizontalMargin() { LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); return lp.getMarginEnd(); } /** - * Updates the z order, positions, and badge visibility of the bubble views in - * the bar based + * Updates the z order, positions, and badge visibility of the bubble views in the bar based * on the expanded state. */ private void updateBubblesLayoutProperties(BubbleBarLocation bubbleBarLocation) { - final float widthState = (float) mWidthAnimator.getAnimatedValue(); + final float widthState; + if (mWidthAnimator == null) { + widthState = mIsBarExpanded ? 1f : 0f; + } else { + widthState = (float) mWidthAnimator.getAnimatedValue(); + } final float currentWidth = getWidth(); final float expandedWidth = expandedWidth(); final float collapsedWidth = collapsedWidth(); - int bubbleCount = getChildCount(); - float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0); - float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); - // When translating X & Y the scale is ignored, so need to deduct it from the - // translations - final float ty = bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift(); - final boolean animate = getVisibility() == VISIBLE; + int childCount = getChildCount(); + final float ty = getBubbleTranslationY(); final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl()); // elevation state is opposite to widthState - when expanded all icons are flat float elevationState = (1 - widthState); - for (int i = 0; i < bubbleCount; i++) { + for (int i = 0; i < childCount; i++) { BubbleView bv = (BubbleView) getChildAt(i); - if (bv == mDraggedBubbleView) { + if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) { // Skip the dragged bubble. Its translation is managed by the drag controller. continue; } @@ -827,45 +1035,52 @@ public class BubbleBarView extends FrameLayout { bv.setDragTranslationX(0f); bv.setOffsetX(0f); - bv.setScaleX(mIconScale); - bv.setScaleY(mIconScale); + if (mBubbleAnimator == null || !mBubbleAnimator.isRunning()) { + // if the bubble animator is running don't set scale here, it will be set by the + // animator + bv.setScaleX(mIconScale); + bv.setScaleY(mIconScale); + } bv.setTranslationY(ty); + // the position of the bubble when the bar is fully expanded - final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft); + final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft); // the position of the bubble when the bar is fully collapsed - final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft); + final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft); // slowly animate elevation while keeping correct Z ordering float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i; bv.setZ(fullElevationForChild * elevationState); + // only update the dot and badge scale if we're expanding or collapsing + if (mWidthAnimator != null && mWidthAnimator.isRunning()) { + // The dot for the selected bubble scales in the opposite direction of the expansion + // animation. + bv.showDotIfNeeded(bv == mSelectedBubbleView ? 1 - widthState : widthState); + // The badge for the selected bubble is always at full scale. All other bubbles + // scale according to the expand animation. + bv.setBadgeScale(bv == mSelectedBubbleView ? 1 : widthState); + } + if (mIsBarExpanded) { // If bar is on the right, account for bubble bar expanding and shifting left final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth; // where the bubble will end up when the animation ends final float targetX = expandedX + expandedBarShift; bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX); - // When we're expanded, we're not stacked so we're not behind the stack - bv.setBehindStack(false, animate); - bv.setAlpha(1); + bv.setVisibility(VISIBLE); } else { // If bar is on the right, account for bubble bar expanding and shifting left final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth; final float targetX = collapsedX + collapsedBarShift; bv.setTranslationX(widthState * (expandedX - targetX) + targetX); - // If we're not the first bubble we're behind the stack - bv.setBehindStack(i > 0, animate); - // If we're fully collapsed, hide all bubbles except for the first 2. If there - // are - // only 2 bubbles, hide the second bubble as well because it's the overflow. + // If we're fully collapsed, hide all bubbles except for the first 2, excluding + // the overflow. if (widthState == 0) { - if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { - bv.setAlpha(0); - } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1 - && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) { - bv.setAlpha(0); + if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { + bv.setVisibility(INVISIBLE); } else { - bv.setAlpha(1); + bv.setVisibility(VISIBLE); } } } @@ -875,7 +1090,8 @@ public class BubbleBarView extends FrameLayout { final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed( bubbleBarLocation); final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded(bubbleBarLocation); - final float interpolatedWidth = widthState * (expandedWidth - collapsedWidth) + collapsedWidth; + final float interpolatedWidth = + widthState * (expandedWidth - collapsedWidth) + collapsedWidth; final float arrowPosition; float interpolatedShift = (expandedArrowPosition - collapsedArrowPosition) * widthState; @@ -887,7 +1103,8 @@ public class BubbleBarView extends FrameLayout { + interpolatedShift; } else { final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition; - arrowPosition = targetPosition + widthState * (expandedArrowPosition - targetPosition); + arrowPosition = + targetPosition + widthState * (expandedArrowPosition - targetPosition); } } mBubbleBarBackground.setArrowPosition(arrowPosition); @@ -906,9 +1123,8 @@ public class BubbleBarView extends FrameLayout { } final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; float translationX; - if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { - translationX = getExpandedBubbleTranslationXDuringScaleAnimation( - bubbleIndex, bubbleCount, onLeft); + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getBubbleTranslationX(bubbleIndex) + mBubbleBarPadding; } else if (onLeft) { translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing; } else { @@ -917,82 +1133,43 @@ public class BubbleBarView extends FrameLayout { return translationX - getScaleIconShift(); } - /** - * Returns the translation X for the bubble at index {@code bubbleIndex} when - * the bubble bar is - * expanded and a new bubble is animating in. - * - *

- * This method assumes that the animation is running so callers are expected to - * verify that - * before calling it. - */ - private float getExpandedBubbleTranslationXDuringScaleAnimation( - int bubbleIndex, int bubbleCount, boolean onLeft) { - // when the new bubble scale animation is running, a new bubble is animating in - // while the - // bubble bar is expanded, so we have at least 2 bubbles in the bubble bar - the - // expanded - // one, and the new one animating in. - - if (mNewBubbleScaleInAnimator == null) { - // callers of this method are expected to verify that the animation is running, - // but the - // compiler doesn't know that. - return 0; - } - final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; - final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); - // the new bubble is scaling in from the center, so we need to adjust its - // translation so - // that the distance to the adjacent bubble scales at the same rate. - final float pivotAdjustment = -(1 - newBubbleScale) * getScaledIconSize() / 2f; - - if (onLeft) { - if (bubbleIndex == 0) { - // this is the animating bubble. use scaled spacing between it and the bubble to - // its left - return (bubbleCount - 1) * getScaledIconSize() - + (bubbleCount - 2) * mExpandedBarIconsSpacing - + newBubbleScale * mExpandedBarIconsSpacing - + pivotAdjustment; - } - // when the bubble bar is on the left, only the translation of the right-most - // bubble - // is affected by the scale animation. - return (bubbleCount - bubbleIndex - 1) * iconAndSpacing; - } else if (bubbleIndex == 0) { - // the bubble bar is on the right, and this is the animating bubble. it only - // needs - // to be adjusted for the scaling pivot. - return pivotAdjustment; - } else { - return iconAndSpacing * (bubbleIndex - 1 + newBubbleScale); - } - } - - private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, - boolean onLeft) { - if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) { + private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) { + if (bubbleIndex < 0 || bubbleIndex >= childCount) { return 0; } float translationX; if (onLeft) { - // Shift the first bubble only if there are more bubbles in addition to overflow - translationX = mBubbleBarPadding + (bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED - ? mIconOverlapAmount - : 0); + // Shift the first bubble only if there are more bubbles + if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) { + translationX = mIconOverlapAmount; + } else { + translationX = 0f; + } } else { - translationX = mBubbleBarPadding + (bubbleIndex == 0 ? 0 : mIconOverlapAmount); + // when the bar is on the right, the first bubble always has translation 0. the only + // case where another bubble has translation 0 is when we only have 1 bubble and the + // overflow. otherwise all other bubbles should be shifted by the overlap amount. + if (bubbleIndex == 0 || getBubbleChildCount() == 1) { + translationX = 0f; + } else { + translationX = mIconOverlapAmount; + } } - return translationX - getScaleIconShift(); + return mBubbleBarPadding + translationX - getScaleIconShift(); + } + + private float getBubbleTranslationY() { + float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0); + float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); + // When translating X & Y the scale is ignored, so need to deduct it from the translations + return mBubbleOffsetY + bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift(); } /** * Reorders the views to match the provided list. */ public void reorder(List viewOrder) { - if (isExpanded() || mWidthAnimator.isRunning()) { + if (isExpanded() || (mWidthAnimator != null && mWidthAnimator.isRunning())) { mReorderRunnable = () -> doReorder(viewOrder); } else { doReorder(viewOrder); @@ -1014,6 +1191,7 @@ public class BubbleBarView extends FrameLayout { } updateBubblesLayoutProperties(mBubbleBarLocation); updateContentDescription(); + updateDotsAndBadgesIfCollapsed(); } } @@ -1022,7 +1200,7 @@ public class BubbleBarView extends FrameLayout { mUpdateSelectedBubbleAfterCollapse = updateSelectedBubbleAfterCollapse; } - void setController(Controller controller) { + public void setController(Controller controller) { mController = controller; } @@ -1033,15 +1211,23 @@ public class BubbleBarView extends FrameLayout { BubbleView previouslySelectedBubble = mSelectedBubbleView; mSelectedBubbleView = view; mBubbleBarBackground.showArrow(view != null); - // TODO: (b/283309949) remove animation should be implemented first, so than - // arrow - // animation is adjusted, skip animation for now - updateArrowForSelected(previouslySelectedBubble != null); + + // if bubbles are being animated, the arrow position will be set as part of the animation + if (mBubbleAnimator == null) { + updateArrowForSelected(previouslySelectedBubble != null); + } + if (view != null) { + if (isExpanded()) { + view.markSeen(); + } else { + // when collapsed, the selected bubble should show the dot if it has it + view.showDotIfNeeded(/* animate= */ true); + } + } } /** - * Sets the dragged bubble view to correctly apply Z order. Dragged view should - * appear on top + * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top */ public void setDraggedBubble(@Nullable BubbleView view) { if (mDraggedBubbleView != null) { @@ -1050,6 +1236,8 @@ public class BubbleBarView extends FrameLayout { mDraggedBubbleView = view; if (view != null) { view.setZ(mDragElevation); + // we started dragging a bubble. reset the bubble that was previously dismissed by drag + mDismissedByDragBubbleView = null; } setIsDragging(view != null); } @@ -1057,18 +1245,15 @@ public class BubbleBarView extends FrameLayout { /** * Update the arrow position to match the selected bubble. * - * @param shouldAnimate whether or not to animate the arrow. If the bar was just - * expanded, this - * should be set to {@code false}. Otherwise set this to - * {@code true}. + * @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this + * should be set to {@code false}. Otherwise set this to {@code true}. */ private void updateArrowForSelected(boolean shouldAnimate) { if (mSelectedBubbleView == null) { Log.w(TAG, "trying to update selection arrow without a selected view!"); return; } - // Find the center of the bubble when it's expanded, set the arrow position to - // it. + // Find the center of the bubble when it's expanded, set the arrow position to it. final float tx = arrowPositionForSelectedWhenExpanded(mBubbleBarLocation); final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX(); if (tx == currentArrowPosition) { @@ -1095,6 +1280,9 @@ public class BubbleBarView extends FrameLayout { } private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) { + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getArrowPosition() + mBubbleBarPadding; + } final int index = indexOfChild(mSelectedBubbleView); final float selectedBubbleTranslationX = getExpandedBubbleTranslationX( index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl())); @@ -1110,8 +1298,7 @@ public class BubbleBarView extends FrameLayout { bubblePosition = index == 0 && getChildCount() > MAX_VISIBLE_BUBBLES_COLLAPSED ? 1 : 0; } else { bubblePosition = index >= MAX_VISIBLE_BUBBLES_COLLAPSED - ? MAX_VISIBLE_BUBBLES_COLLAPSED - 1 - : index; + ? MAX_VISIBLE_BUBBLES_COLLAPSED - 1 : index; } return mBubbleBarPadding + bubblePosition * (mIconOverlapAmount) + getScaledIconSize() / 2f; } @@ -1123,32 +1310,46 @@ public class BubbleBarView extends FrameLayout { } /** - * The click listener used for the bubble view gets added / removed depending on - * whether - * the bar is expanded or collapsed, this updates whether the listener is set - * based on state. + * The click listener used for the bubble view gets added / removed depending on whether + * the bar is expanded or collapsed, this updates whether the listener is set based on state. */ private void setOrUnsetClickListener() { super.setOnClickListener(mIsBarExpanded ? null : mOnClickListener); } /** - * Sets whether the bubble bar is expanded or collapsed. + * Update bubble bar expanded state. */ public void setExpanded(boolean isBarExpanded) { - if (mIsBarExpanded != isBarExpanded) { - mIsBarExpanded = isBarExpanded; - updateArrowForSelected(/* shouldAnimate= */ false); - setOrUnsetClickListener(); - if (isBarExpanded) { - mWidthAnimator.start(); - } else { - mWidthAnimator.reverse(); - } - updateBubbleAccessibilityStates(); - } + setExpandedInternal(isBarExpanded, false); } + /** + * Update bubble bar expanded state with animation. + *

+ * Also triggers a talkback announcement for accessibility. + */ + public void animateExpanded(boolean isBarExpanded) { + setExpandedInternal(isBarExpanded, true); + } + + private void setExpandedInternal(boolean isBarExpanded, boolean animate) { + if (mIsBarExpanded == isBarExpanded) return; + mIsBarExpanded = isBarExpanded; + updateArrowForSelected(/* shouldAnimate= */ false); + setOrUnsetClickListener(); + if (animate) { + mWidthAnimator = createExpansionAnimator(isBarExpanded); + mWidthAnimator.start(); + announceExpandedStateChange(); + } else { + onExpandedChanged(); + } + updateBubbleAccessibilityStates(); + mController.onBubbleBarExpandedStateChanged(mIsBarExpanded); + } + + /** * Returns whether the bubble bar is expanded. */ @@ -1156,59 +1357,79 @@ public class BubbleBarView extends FrameLayout { return mIsBarExpanded; } + /** + * Returns whether the bubble bar is expanding. + */ + public boolean isExpanding() { + return mWidthAnimator != null && mWidthAnimator.isRunning() && mIsBarExpanded; + } + /** * Get width of the bubble bar as if it would be expanded. * - * @return width of the bubble bar in its expanded state, regardless of current - * width + * @return width of the bubble bar in its expanded state, regardless of current width */ public float expandedWidth() { final int childCount = getChildCount(); - // spaces amount is less than child count by 1, or 0 if no child views - final float totalSpace; - final float totalIconSize; - if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { - // when this animation is running, a new bubble is animating in while the bubble - // bar is - // expanded, so we have at least 2 bubbles in the bubble bar. - final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); - totalSpace = (childCount - 2 + newBubbleScale) * mExpandedBarIconsSpacing; - totalIconSize = (childCount - 1 + newBubbleScale) * getScaledIconSize(); - } else { - totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; - totalIconSize = childCount * getScaledIconSize(); + final float horizontalPadding = 2 * mBubbleBarPadding; + if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { + return mBubbleAnimator.getExpandedWidth() + horizontalPadding; } - return totalIconSize + totalSpace + 2 * mBubbleBarPadding; + // spaces amount is less than child count by 1, or 0 if no child views + final float totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; + final float totalIconSize = childCount * getScaledIconSize(); + return totalIconSize + totalSpace + horizontalPadding; } - private float collapsedWidth() { - final int childCount = getChildCount(); + /** + * Get width of the bubble bar if it is collapsed + */ + float collapsedWidth() { + final int bubbleChildCount = getBubbleChildCount(); final float horizontalPadding = 2 * mBubbleBarPadding; - // If there are more than 2 bubbles, the first 2 should be visible when - // collapsed. - // Otherwise just the first bubble should be visible because we don't show the - // overflow. - return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED - ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding + // If there are more than 2 bubbles, the first 2 should be visible when collapsed, + // excluding the overflow. + return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED + ? getCollapsedWidthWithMaxVisibleBubbles() : getScaledIconSize() + horizontalPadding; } + float getCollapsedWidthWithMaxVisibleBubbles() { + return getScaledIconSize() + mIconOverlapAmount + 2 * mBubbleBarPadding; + } + + float getCollapsedWidthForIconSizeAndPadding(int iconSize, int bubbleBarPadding) { + final int bubbleChildCount = Math.min(getBubbleChildCount(), MAX_VISIBLE_BUBBLES_COLLAPSED); + if (bubbleChildCount == 0) return 0; + final int spacesCount = bubbleChildCount - 1; + final float horizontalPadding = 2 * bubbleBarPadding; + return iconSize * bubbleChildCount + mIconOverlapAmount * spacesCount + horizontalPadding; + } + + /** Returns the child count excluding the overflow if it's present. */ + int getBubbleChildCount() { + return hasOverflow() ? getChildCount() - 1 : getChildCount(); + } + private float getBubbleBarExpandedHeight() { return getBubbleBarCollapsedHeight() + mPointerSize; } + float getArrowHeight() { + return mPointerSize; + } + float getBubbleBarCollapsedHeight() { // the pointer is invisible when collapsed return getScaledIconSize() + mBubbleBarPadding * 2; } /** - * Returns whether the given MotionEvent, *in screen coordinates*, is within - * bubble bar + * Returns whether the given MotionEvent, *in screen coordinates*, is within bubble bar * touch bounds. */ public boolean isEventOverAnyItem(MotionEvent ev) { - if (getVisibility() == View.VISIBLE) { + if (getVisibility() == VISIBLE) { getBoundsOnScreen(mTempRect); return mTempRect.contains((int) ev.getX(), (int) ev.getY()); } @@ -1217,9 +1438,7 @@ public class BubbleBarView extends FrameLayout { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - if (mIsAnimatingNewBubble) { - mController.onBubbleBarTouchedWhileAnimating(); - } + mController.onBubbleBarTouched(); if (!mIsBarExpanded) { // When the bar is collapsed, all taps on it should expand it. return true; @@ -1227,13 +1446,8 @@ public class BubbleBarView extends FrameLayout { return super.onInterceptTouchEvent(ev); } - /** Whether a new bubble is currently animating. */ - public boolean isAnimatingNewBubble() { - return mIsAnimatingNewBubble; - } - - private boolean hasOverview() { - // Overview is always the last bubble + private boolean hasOverflow() { + // Overflow is always the last bubble View lastChild = getChildAt(getChildCount() - 1); if (lastChild instanceof BubbleView bubbleView) { return bubbleView.getBubble() instanceof BubbleBarOverflow; @@ -1242,22 +1456,39 @@ public class BubbleBarView extends FrameLayout { } private void updateBubbleAccessibilityStates() { - final int childA11y; if (mIsBarExpanded) { // Bar is expanded, focus on the bubbles setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_YES; + + // Set up a11y navigation order. Get list of child views in the order they are shown + // on screen. And use that to set up navigation so that swiping left focuses the view + // on the left and swiping right focuses view on the right. + View prevChild = null; + for (View childView : getChildViewsInOnScreenOrder()) { + childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + childView.setFocusable(true); + final View finalPrevChild = prevChild; + // Always need to set a new delegate to clear out any previous. + childView.setAccessibilityDelegate(new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (finalPrevChild != null) { + info.setTraversalAfter(finalPrevChild); + } + } + }); + prevChild = childView; + } } else { // Bar is collapsed, only focus on the bar setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_NO; - } - for (int i = 0; i < getChildCount(); i++) { - getChildAt(i).setImportantForAccessibility(childA11y); - // Only allowing focusing on bubbles when bar is expanded. Otherwise, in - // talkback mode, - // bubbles can be navigates to in collapsed mode. - getChildAt(i).setFocusable(mIsBarExpanded); + for (int i = 0; i < getChildCount(); i++) { + View childView = getChildAt(i); + childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + childView.setFocusable(false); + } } } @@ -1266,7 +1497,7 @@ public class BubbleBarView extends FrameLayout { CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : ""; // Don't count overflow if it exists - int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0); + int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0); if (bubbleCount > 1) { contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles, contentDesc, bubbleCount - 1); @@ -1274,6 +1505,26 @@ public class BubbleBarView extends FrameLayout { setContentDescription(contentDesc); } + private void announceExpandedStateChange() { + final CharSequence selectedBubbleContentDesc; + if (mSelectedBubbleView != null) { + selectedBubbleContentDesc = mSelectedBubbleView.getContentDescription(); + } else { + selectedBubbleContentDesc = getResources().getString( + R.string.bubble_bar_bubble_fallback_description); + } + + final String msg; + if (mIsBarExpanded) { + msg = getResources().getString(R.string.bubble_bar_accessibility_announce_expand, + selectedBubbleContentDesc); + } else { + msg = getResources().getString(R.string.bubble_bar_accessibility_announce_collapse, + selectedBubbleContentDesc); + } + announceForAccessibility(msg); + } + private boolean isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding) { return isIconSizeUpdated(newIconSize) || isPaddingUpdated(newBubbleBarPadding); } @@ -1290,8 +1541,7 @@ public class BubbleBarView extends FrameLayout { @Nullable Runnable onStart, @Nullable Runnable onEnd, @Nullable ValueAnimator.AnimatorUpdateListener onUpdate) { - if (onUpdate != null) - animator.addUpdateListener(onUpdate); + if (onUpdate != null) animator.addUpdateListener(onUpdate); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationCancel(Animator animator) { @@ -1300,14 +1550,12 @@ public class BubbleBarView extends FrameLayout { @Override public void onAnimationStart(Animator animator) { - if (onStart != null) - onStart.run(); + if (onStart != null) onStart.run(); } @Override public void onAnimationEnd(Animator animator) { - if (onEnd != null) - onEnd.run(); + if (onEnd != null) onEnd.run(); } @Override @@ -1317,16 +1565,135 @@ public class BubbleBarView extends FrameLayout { }); } + /** Dumps the current state of BubbleBarView. */ + public void dump(PrintWriter pw) { + pw.println("BubbleBarView state:"); + pw.println(" visibility: " + getVisibility()); + pw.println(" alpha: " + getAlpha()); + pw.println(" translationY: " + getTranslationY()); + pw.println(" childCount: " + getChildCount()); + pw.println(" hasOverflow: " + hasOverflow()); + for (BubbleView bubbleView: getBubbles()) { + BubbleBarItem bubble = bubbleView.getBubble(); + String key = bubble == null ? "null" : bubble.getKey(); + pw.println(" bubble key: " + key); + } + pw.println(" isExpanded: " + isExpanded()); + if (mBubbleAnimator != null) { + pw.println(" mBubbleAnimator.isRunning(): " + mBubbleAnimator.isRunning()); + pw.println(" mBubbleAnimator is null"); + } + pw.println(" mDragging: " + mDragging); + } + + private List getBubbles() { + List bubbles = new ArrayList<>(); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof BubbleView bubble) { + bubbles.add(bubble); + } + } + return bubbles; + } + + /** Creates an animator based on the expanding or collapsing action. */ + private ValueAnimator createExpansionAnimator(boolean expanding) { + float startValue = expanding ? 0 : 1; + if ((mWidthAnimator != null && mWidthAnimator.isRunning())) { + startValue = (float) mWidthAnimator.getAnimatedValue(); + mWidthAnimator.cancel(); + } + float endValue = expanding ? 1 : 0; + ValueAnimator animator = ValueAnimator.ofFloat(startValue, endValue); + animator.setDuration(WIDTH_ANIMATION_DURATION_MS); + animator.setInterpolator(Interpolators.EMPHASIZED); + addAnimationCallBacks(animator, + /* onStart= */ () -> mBubbleBarBackground.showArrow(true), + /* onEnd= */ this::onExpandedChanged, + /* onUpdate= */ anim -> { + updateBubblesLayoutProperties(mBubbleBarLocation); + invalidate(); + }); + return animator; + } + + private void onExpandedChanged() { + mBubbleBarBackground.showArrow(mIsBarExpanded); + if (!mIsBarExpanded && mReorderRunnable != null) { + mReorderRunnable.run(); + mReorderRunnable = null; + } + // If the bar was just collapsed and the overflow was the last bubble that was + // selected, set the first bubble as selected. + if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null + && mSelectedBubbleView != null + && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) { + BubbleView firstBubble = (BubbleView) getChildAt(0); + mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey()); + } + // If the bar was just expanded, remove the dot from the selected bubble. + if (mIsBarExpanded && mSelectedBubbleView != null) { + mSelectedBubbleView.markSeen(); + } + updateLayoutParams(); + } + + /** + * Returns the distance between the top left corner of the bubble bar to the center of the dot + * of the selected bubble. + */ + PointF getSelectedBubbleDotDistanceFromTopLeft() { + if (mSelectedBubbleView == null) { + return new PointF(0, 0); + } + final int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView); + final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); + final float selectedBubbleTx = isExpanded() + ? getExpandedBubbleTranslationX(indexOfSelectedBubble, getChildCount(), onLeft) + : getCollapsedBubbleTranslationX(indexOfSelectedBubble, getChildCount(), onLeft); + PointF selectedBubbleDotCenter = mSelectedBubbleView.getDotCenter(); + + return new PointF( + selectedBubbleTx + selectedBubbleDotCenter.x, + mBubbleBarPadding + mPointerSize + selectedBubbleDotCenter.y); + } + + int getSelectedBubbleDotColor() { + return mSelectedBubbleView == null ? 0 : mSelectedBubbleView.getDotColor(); + } + + int getPointerSize() { + return mPointerSize; + } + + float getBubbleElevation() { + return mBubbleElevation; + } + /** Interface for BubbleBarView to communicate with its controller. */ - interface Controller { + public interface Controller { /** Returns the translation Y that the bubble bar should have. */ float getBubbleBarTranslationY(); - /** - * Notifies the controller that the bubble bar was touched while it was - * animating. - */ - void onBubbleBarTouchedWhileAnimating(); + /** Notifies the controller that the bubble bar was touched. */ + void onBubbleBarTouched(); + + /** Requests the controller to expand bubble bar */ + void expandBubbleBar(); + + /** Requests the controller to dismiss the bubble bar */ + void dismissBubbleBar(); + + /** Requests the controller to update bubble bar location to the given value */ + void updateBubbleBarLocation(BubbleBarLocation location, + @BubbleBarLocation.UpdateSource int source); + + /** Notifies the controller that bubble bar is being dragged */ + void setIsDragging(boolean dragging); + + /** Notifies the controller that bubble bar expanded state changed */ + void onBubbleBarExpandedStateChanged(boolean expanded); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 8e25c5c5d1..648ad9f2a2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -18,6 +18,14 @@ package com.android.launcher3.taskbar.bubbles; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; +import static com.android.launcher3.Utilities.mapRange; +import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT; +import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.content.Intent; +import android.content.pm.ShortcutInfo; import static java.util.stream.Collectors.toList; import android.content.res.Resources; @@ -27,7 +35,6 @@ import android.graphics.Rect; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; @@ -35,25 +42,34 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.app.animation.Interpolators; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarControllers; import com.android.launcher3.taskbar.TaskbarInsetsController; +import com.android.launcher3.taskbar.TaskbarSharedState; import com.android.launcher3.taskbar.TaskbarStashController; import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator; +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController; +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner; +import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; import com.android.quickstep.SystemUiProxy; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.Flags; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import java.io.PrintWriter; import java.util.List; import java.util.Objects; import java.util.function.Consumer; /** - * Controller for {@link BubbleBarView}. Manages the visibility of the bubble - * bar as well as + * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as * responding to changes in bubble state provided by BubbleBarController. */ public class BubbleBarViewController { @@ -62,10 +78,17 @@ public class BubbleBarViewController { private static final float APP_ICON_SMALL_DP = 44f; private static final float APP_ICON_MEDIUM_DP = 48f; private static final float APP_ICON_LARGE_DP = 52f; + /** The dot size is defined as a percentage of the icon size. */ + private static final float DOT_TO_BUBBLE_SIZE_RATIO = 0.228f; + public static final int TASKBAR_FADE_IN_DURATION_MS = 150; + public static final int TASKBAR_FADE_IN_DELAY_MS = 50; + public static final int TASKBAR_FADE_OUT_DURATION_MS = 100; private final SystemUiProxy mSystemUiProxy; private final TaskbarActivityContext mActivity; private final BubbleBarView mBarView; private int mIconSize; + private int mBubbleBarPadding; + private final int mDragElevation; // Initialized in init. private BubbleStashController mBubbleStashController; @@ -73,55 +96,108 @@ public class BubbleBarViewController { private BubbleDragController mBubbleDragController; private TaskbarStashController mTaskbarStashController; private TaskbarInsetsController mTaskbarInsetsController; + private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider; private View.OnClickListener mBubbleClickListener; - private View.OnClickListener mBubbleBarClickListener; + private BubbleView.Controller mBubbleViewController; + private BubbleBarOverflow mOverflowBubble; - // These are exposed to {@link BubbleStashController} to animate for - // stashing/un-stashing + // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing private final MultiValueAlpha mBubbleBarAlpha; - private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale); + private final AnimatedFloat mBubbleBarBubbleAlpha = new AnimatedFloat(this::updateBubbleAlpha); + private final AnimatedFloat mBubbleBarBackgroundAlpha = new AnimatedFloat( + this::updateBackgroundAlpha); + private final AnimatedFloat mBubbleBarScaleX = new AnimatedFloat(this::updateScaleX); + private final AnimatedFloat mBubbleBarScaleY = new AnimatedFloat(this::updateScaleY); + private final AnimatedFloat mBubbleBarBackgroundScaleX = new AnimatedFloat( + this::updateBackgroundScaleX); + private final AnimatedFloat mBubbleBarBackgroundScaleY = new AnimatedFloat( + this::updateBackgroundScaleY); private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat( this::updateTranslationY); + private final AnimatedFloat mBubbleOffsetY = new AnimatedFloat( + this::updateBubbleOffsetY); + private final AnimatedFloat mBubbleBarPinning = new AnimatedFloat(pinningProgress -> { + updateTranslationY(); + setBubbleBarScaleAndPadding(pinningProgress); + }); // Modified when swipe up is happening on the bubble bar or task bar. private float mBubbleBarSwipeUpTranslationY; - + // Modified when bubble bar is springing back into the stash handle. + private float mBubbleBarStashTranslationY; + // Minimum distance between the BubbleBar and the taskbar + private final int mBubbleBarTaskbarMinDistance; // Whether the bar is hidden for a sysui state. private boolean mHiddenForSysui; // Whether the bar is hidden because there are no bubbles. private boolean mHiddenForNoBubbles = true; + // Whether the bar is hidden when stashed + private boolean mHiddenForStashed; private boolean mShouldShowEducation; + public boolean mOverflowAdded; + private boolean mWasStashedBeforeEnteringBubbleDragZone = false; + + /** This field is used solely to track the bubble bar location prior to the start of the drag */ + private @Nullable BubbleBarLocation mBubbleBarDragLocation; private BubbleBarViewAnimator mBubbleBarViewAnimator; + private final FrameLayout mBubbleBarContainer; + private BubbleBarFlyoutController mBubbleBarFlyoutController; + private BubbleBarPinController mBubbleBarPinController; + private TaskbarSharedState mTaskbarSharedState; + private final TimeSource mTimeSource = System::currentTimeMillis; + private final int mTaskbarTranslationDelta; @Nullable private BubbleBarBoundsChangeListener mBoundsChangeListener; - public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) { + public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView, + FrameLayout bubbleBarContainer) { mActivity = activity; mBarView = barView; + mBubbleBarContainer = bubbleBarContainer; mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity); mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */); - mIconSize = activity.getResources().getDimensionPixelSize( - R.dimen.bubblebar_icon_size); + Resources res = activity.getResources(); + mIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size); + mBubbleBarTaskbarMinDistance = res.getDimensionPixelSize( + R.dimen.bubblebar_transient_taskbar_min_distance); + mDragElevation = res.getDimensionPixelSize(R.dimen.dragged_bubble_elevation); + mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity); } - public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { + /** Initializes controller. */ + public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers, + TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) { + mTaskbarSharedState = controllers.getSharedState(); mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleBarController = bubbleControllers.bubbleBarController; mBubbleDragController = bubbleControllers.bubbleDragController; + mBubbleBarPinController = bubbleControllers.bubbleBarPinController; mTaskbarStashController = controllers.taskbarStashController; mTaskbarInsetsController = controllers.taskbarInsetsController; - mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController); - + mBubbleBarFlyoutController = new BubbleBarFlyoutController( + mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks()); + mBubbleBarViewAnimator = new BubbleBarViewAnimator( + mBarView, mBubbleStashController, mBubbleBarFlyoutController, + createBubbleBarParentViewController(), mBubbleBarController::showExpandedView, + () -> setHiddenForBubbles(false)); + mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider; + onBubbleBarConfigurationChanged(/* animate= */ false); mActivity.addOnDeviceProfileChangeListener( - dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true)); - updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false); - mBubbleBarScale.updateValue(1f); - mBubbleClickListener = v -> onBubbleClicked(v); - mBubbleBarClickListener = v -> onBubbleBarClicked(); + dp -> onBubbleBarConfigurationChanged(/* animate= */ true)); + mBubbleBarScaleY.updateValue(1f); + mBubbleClickListener = v -> onBubbleClicked((BubbleView) v); mBubbleDragController.setupBubbleBarView(mBarView); - mBarView.setOnClickListener(mBubbleBarClickListener); + mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView); + if (!Flags.enableOptionalBubbleOverflow()) { + showOverflow(true); + } + if (!mBubbleStashController.isTransientTaskBar()) { + // TODO(b/380274085) for transient taskbar mode, the click is also handled by the input + // consumer. This check can be removed once b/380274085 is fixed. + mBarView.setOnClickListener(v -> animateExpanded(!mBarView.isExpanded())); + } mBarView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); @@ -129,21 +205,170 @@ public class BubbleBarViewController { mBoundsChangeListener.onBoundsChanged(); } }); + float pinningValue = mActivity.isTransientTaskbar() + ? PINNING_TRANSIENT + : PINNING_PERSISTENT; + mBubbleBarPinning.updateValue(pinningValue); mBarView.setController(new BubbleBarView.Controller() { @Override public float getBubbleBarTranslationY() { - return mBubbleStashController.getBubbleBarTranslationY(); + return mBubbleStashController.getTargetTranslationYForState(); } @Override - public void onBubbleBarTouchedWhileAnimating() { - BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating(); + public void onBubbleBarTouched() { + if (isAnimatingNewBubble()) { + interruptAnimationForTouch(); + } + } + + @Override + public void expandBubbleBar() { + BubbleBarViewController.this.animateExpanded( + /* isExpanded= */ true, /* maybeShowEdu*/ true); + } + + @Override + public void dismissBubbleBar() { + onDismissAllBubbles(); + } + + @Override + public void updateBubbleBarLocation(BubbleBarLocation location, + @BubbleBarLocation.UpdateSource int source) { + mBubbleBarController.updateBubbleBarLocation(location, source); + } + + @Override + public void setIsDragging(boolean dragging) { + mBubbleBarContainer.setElevation(dragging ? mDragElevation : 0); + } + + @Override + public void onBubbleBarExpandedStateChanged(boolean expanded) { + if (expanded && !mTaskbarStashController.isStashed()) { + mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */, + false /* shouldBubblesFollow */); + } } }); + + mBubbleViewController = new BubbleView.Controller() { + @Override + public BubbleBarLocation getBubbleBarLocation() { + return BubbleBarViewController.this.getBubbleBarLocation(); + } + + @Override + public void dismiss(BubbleView bubble) { + if (bubble.getBubble() != null) { + notifySysUiBubbleDismissed(bubble.getBubble()); + } + onBubbleDismissed(bubble); + } + + @Override + public void collapse() { + collapseBubbleBar(); + } + + @Override + public void updateBubbleBarLocation(BubbleBarLocation location, + @BubbleBarLocation.UpdateSource int source) { + mBubbleBarController.updateBubbleBarLocation(location, source); + } + }; } - private void onBubbleClicked(View v) { - BubbleBarItem bubble = ((BubbleView) v).getBubble(); + /** Returns animated float property responsible for pinning transition animation. */ + public AnimatedFloat getBubbleBarPinning() { + return mBubbleBarPinning; + } + + private BubbleBarFlyoutPositioner createFlyoutPositioner() { + return new BubbleBarFlyoutPositioner() { + + @Override + public boolean isOnLeft() { + boolean shouldRevertLocation = + mBarView.isShowingDropTarget() && isLocationUpdatedForDropTarget(); + boolean isOnLeft = mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl()); + return shouldRevertLocation != isOnLeft; + } + + @Override + public float getTargetTy() { + return mBarView.getTranslationY() - mBarView.getHeight(); + } + + @Override + @NonNull + public PointF getDistanceToCollapsedPosition() { + // the flyout animates from the selected bubble dot. calculate the distance it needs + // to translate itself to its starting position. + PointF distanceToDotCenter = mBarView.getSelectedBubbleDotDistanceFromTopLeft(); + + // if we're gravitating left, return the distance between the top left corner of the + // bubble bar and the bottom left corner of the dot. + // if we're gravitating right, return the distance between the top right corner of + // the bubble bar and the bottom right corner of the dot. + float distanceX = isOnLeft() + ? distanceToDotCenter.x - getCollapsedSize() / 2 + : mBarView.getWidth() - distanceToDotCenter.x - getCollapsedSize() / 2; + float distanceY = distanceToDotCenter.y + getCollapsedSize() / 2; + return new PointF(distanceX, distanceY); + } + + @Override + public float getCollapsedSize() { + return mIconSize * DOT_TO_BUBBLE_SIZE_RATIO; + } + + @Override + public int getCollapsedColor() { + return mBarView.getSelectedBubbleDotColor(); + } + + @Override + public float getCollapsedElevation() { + return mBarView.getBubbleElevation(); + } + + @Override + public float getDistanceToRevealTriangle() { + return getDistanceToCollapsedPosition().y - mBarView.getPointerSize(); + } + }; + } + + private FlyoutCallbacks createFlyoutCallbacks() { + return new FlyoutCallbacks() { + @Override + public void flyoutClicked() { + interruptAnimationForTouch(); + animateExpanded(/* isExpanded= */ true, /* maybeShowEdu*/ true); + } + }; + } + + private BubbleBarParentViewHeightUpdateNotifier createBubbleBarParentViewController() { + return new BubbleBarParentViewHeightUpdateNotifier() { + @Override + public void updateTopBoundary() { + mActivity.setTaskbarWindowForAnimatingBubble(); + } + }; + } + + /** Returns the overflow bubble. */ + public BubbleBarOverflow getOverflowBubble() { + return mOverflowBubble; + } + + private void onBubbleClicked(BubbleView bubbleView) { + if (mBubbleBarPinning.isAnimating()) return; + bubbleView.markSeen(); + BubbleBarItem bubble = bubbleView.getBubble(); if (bubble == null) { Log.e(TAG, "bubble click listener, bubble was null"); } @@ -151,36 +376,21 @@ public class BubbleBarViewController { final String currentlySelected = mBubbleBarController.getSelectedBubbleKey(); if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) { // Tapping the currently selected bubble while expanded collapses the view. - setExpanded(false); - mBubbleStashController.stashBubbleBar(); + collapseBubbleBar(); } else { mBubbleBarController.showAndSelectBubble(bubble); } } - private void onBubbleBarTouchedWhileAnimating() { - mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating(); + /** Interrupts the running animation for a touch event on the bubble bar or flyout. */ + private void interruptAnimationForTouch() { + mBubbleBarViewAnimator.interruptForTouch(); mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY()); } - private void onBubbleBarClicked() { - if (mShouldShowEducation) { - mShouldShowEducation = false; - // Get the bubble bar bounds on screen - Rect bounds = new Rect(); - mBarView.getBoundsOnScreen(bounds); - // Calculate user education reference position in Screen coordinates - Point position = new Point(bounds.centerX(), bounds.top); - // Show user education relative to the reference point - mSystemUiProxy.showUserEducation(position); - } else { - // ensure that the bubble bar has the correct translation. we may have just - // interrupted - // the animation by touching the bubble bar. - mBubbleBarTranslationY.animateToValue(mBubbleStashController.getBubbleBarTranslationY()) - .start(); - setExpanded(true); - } + private void collapseBubbleBar() { + animateExpanded(false); + mBubbleStashController.stashBubbleBar(); } /** Notifies that the stash state is changing. */ @@ -190,9 +400,31 @@ public class BubbleBarViewController { } } + /** Shows the education view if it was previously requested. */ + private boolean maybeShowEduView() { + if (mShouldShowEducation) { + mShouldShowEducation = false; + // Get the bubble bar bounds on screen + Rect bounds = new Rect(); + mBarView.getBoundsOnScreen(bounds); + // Calculate user education reference position in Screen coordinates + Point position = new Point(bounds.centerX(), bounds.top); + // Show user education relative to the reference point + mSystemUiProxy.showUserEducation(position); + return true; + } + return false; + } + + /** Notifies that the IME became visible. */ + public void onImeVisible() { + if (isAnimatingNewBubble()) { + mBubbleBarViewAnimator.interruptForIme(); + } + } + // - // The below animators are exposed to BubbleStashController so it can manage the - // stashing + // The below animators are exposed to BubbleStashController so it can manage the stashing // animation. // @@ -200,18 +432,72 @@ public class BubbleBarViewController { return mBubbleBarAlpha; } - public AnimatedFloat getBubbleBarScale() { - return mBubbleBarScale; + public AnimatedFloat getBubbleBarBubbleAlpha() { + return mBubbleBarBubbleAlpha; + } + + public AnimatedFloat getBubbleBarBackgroundAlpha() { + return mBubbleBarBackgroundAlpha; + } + + public AnimatedFloat getBubbleBarScaleX() { + return mBubbleBarScaleX; + } + + public AnimatedFloat getBubbleBarScaleY() { + return mBubbleBarScaleY; + } + + public AnimatedFloat getBubbleBarBackgroundScaleX() { + return mBubbleBarBackgroundScaleX; + } + + public AnimatedFloat getBubbleBarBackgroundScaleY() { + return mBubbleBarBackgroundScaleY; } public AnimatedFloat getBubbleBarTranslationY() { return mBubbleBarTranslationY; } - float getBubbleBarCollapsedHeight() { + public AnimatedFloat getBubbleOffsetY() { + return mBubbleOffsetY; + } + + public float getBubbleBarCollapsedWidth() { + return mBarView.collapsedWidth(); + } + + public float getBubbleBarCollapsedHeight() { return mBarView.getBubbleBarCollapsedHeight(); } + /** Returns the bubble bar arrow height.*/ + public float getBubbleBarArrowHeight() { + return mBarView.getArrowHeight(); + } + + /** + * @see BubbleBarView#getRelativePivotX() + */ + public float getBubbleBarRelativePivotX() { + return mBarView.getRelativePivotX(); + } + + /** + * @see BubbleBarView#getRelativePivotY() + */ + public float getBubbleBarRelativePivotY() { + return mBarView.getRelativePivotY(); + } + + /** + * @see BubbleBarView#setRelativePivot(float, float) + */ + public void setBubbleBarRelativePivot(float x, float y) { + mBarView.setRelativePivot(x, y); + } + /** * Whether the bubble bar is visible or not. */ @@ -221,7 +507,7 @@ public class BubbleBarViewController { /** Whether the bubble bar has bubbles. */ public boolean hasBubbles() { - return mBubbleBarController.getSelectedBubbleKey() != null; + return mBarView.getBubbleChildCount() > 0; } /** @@ -231,6 +517,21 @@ public class BubbleBarViewController { return mBarView.getBubbleBarLocation(); } + /** + * @return the max collapsed width for the bubble bar. + */ + public float getCollapsedWidthWithMaxVisibleBubbles() { + return mBarView.getCollapsedWidthWithMaxVisibleBubbles(); + } + + /** + * @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on + * the right + */ + public boolean isBubbleBarOnLeft() { + return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl()); + } + /** * Update bar {@link BubbleBarLocation} */ @@ -239,16 +540,114 @@ public class BubbleBarViewController { } /** - * Animate bubble bar to the given location. The location change is transient. - * It does not + * Animate bubble bar to the given location. The location change is transient. It does not * update the state of the bubble bar. - * To update bubble bar pinned location, use - * {@link #setBubbleBarLocation(BubbleBarLocation)}. + * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. */ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { mBarView.animateToBubbleBarLocation(bubbleBarLocation); } + /** Return animator for animating bubble bar in. */ + public Animator animateBubbleBarLocationIn(BubbleBarLocation fromLocation, + BubbleBarLocation toLocation) { + return mBarView.animateToBubbleBarLocationIn(fromLocation, toLocation); + } + + /** Return animator for animating bubble bar out. */ + public Animator animateBubbleBarLocationOut(BubbleBarLocation toLocation) { + return mBarView.animateToBubbleBarLocationOut(toLocation); + } + + /** Returns whether the Bubble Bar is currently displaying a drop target. */ + public boolean isShowingDropTarget() { + return mBarView.isShowingDropTarget(); + } + + /** Tells bubble bar view if it should show the drop target. */ + public void setShowingDropTarget(boolean showingDropTarget) { + mBarView.showDropTarget(showingDropTarget); + } + + //TODO(b/411505605) remove unused IPC calls and code + /** + * Notifies the controller that a drag event is over the Bubble Bar drop zone. The controller + * will display the appropriate drop target and enter drop target mode. The controller will also + * update the return value of {@link #isLocationUpdatedForDropTarget()} to true if location was + * updated. + */ + public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) { + mBubbleBarDragLocation = bubbleBarLocation; + mBarView.showDropTarget(/* isDropTarget = */ true); + mWasStashedBeforeEnteringBubbleDragZone = hasBubbles() + && mBubbleStashController.isStashed(); + if (mWasStashedBeforeEnteringBubbleDragZone) { + // bubble bar is stashed - un-stash at drag location + mBubbleStashController.showBubbleBarAtLocation( + /* fromLocation = */ getBubbleBarLocation(), + /* toLocation = */ mBubbleBarDragLocation + ); + } else if (hasBubbles()) { + if (isLocationUpdatedForDropTarget()) { + // bubble bar has bubbles and location is changed - animate bar to the opposite side + animateBubbleBarLocation(bubbleBarLocation); + } + } else { + // bubble bar has no bubbles flow just show the empty drop target + mBubbleBarPinController.showDropTarget(bubbleBarLocation); + } + } + + /** + * Returns {@code true} if location was updated after most recent + * {@link #onDragItemOverBubbleBarDragZone}}. + */ + public boolean isLocationUpdatedForDropTarget() { + if (mBubbleBarDragLocation == null) { + return false; + } + boolean isRtl = mBarView.isLayoutRtl(); + return getBubbleBarLocation().isOnLeft(isRtl) + != mBubbleBarDragLocation.isOnLeft(isRtl); + } + + /** + * Notifies the controller that the drag event is outside the Bubble Bar drop zone. + * This will hide the drop target zone if there are no bubbles or return the + * Bubble Bar to its original location. The controller will also exit drop target + * mode and reset the value returned from {@link #isLocationUpdatedForDropTarget()} to false. + */ + public void onItemDraggedOutsideBubbleBarDropZone() { + if (!isShowingDropTarget()) { + return; + } + if (mWasStashedBeforeEnteringBubbleDragZone && mBubbleBarDragLocation != null) { + // bubble bar was stashed - stash at original location + mBubbleStashController.stashBubbleBarToLocation( + /* fromLocation = */ mBubbleBarDragLocation, + /* toLocation = */ getBubbleBarLocation() + ); + } else if (hasBubbles()) { + if (isLocationUpdatedForDropTarget()) { + // bubble bar has bubbles and location was changed - return to the original + // location + animateBubbleBarLocation(getBubbleBarLocation()); + } + } + onItemDragCompleted(); + } + + /** + * Notifies the controller that the drag has completed over the Bubble Bar drop zone. + * The controller will hide the drop target if there are no bubbles and exit drop target mode. + */ + public void onItemDragCompleted() { + mBarView.showDropTarget(/* isDropTarget = */ false); + mBubbleBarPinController.hideDropTarget(); + mWasStashedBeforeEnteringBubbleDragZone = false; + mBubbleBarDragLocation = null; + } + /** * The bounds of the bubble bar. */ @@ -256,9 +655,29 @@ public class BubbleBarViewController { return mBarView.getBubbleBarBounds(); } + /** Returns the bounds of the flyout view if it exists, or {@code null} otherwise. */ + @Nullable + public Rect getFlyoutBounds() { + return mBubbleBarFlyoutController.getFlyoutBounds(); + } + + /** Checks that bubble bar is visible and that the motion event is within bounds. */ + public boolean isEventOverBubbleBar(MotionEvent event) { + if (!isBubbleBarVisible()) return false; + final Rect bounds = getBubbleBarBounds(); + final int bubbleBarTopOnScreen = + mActivity.getScreenSize().y - mBarView.getTopToScreenBottom(); + final float x = event.getX(); + return event.getRawY() >= bubbleBarTopOnScreen && x >= bounds.left && x <= bounds.right; + } + /** Whether a new bubble is animating. */ public boolean isAnimatingNewBubble() { - return mBarView.isAnimatingNewBubble(); + return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.isAnimating(); + } + + public boolean isNewBubbleAnimationRunningOrPending() { + return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimation(); } /** The horizontal margin of the bubble bar from the edge of the screen. */ @@ -267,10 +686,8 @@ public class BubbleBarViewController { } /** - * When the bubble bar is not stashed, it can be collapsed (the icons are in a - * stack) or - * expanded (the icons are in a row). This indicates whether the bubble bar is - * expanded. + * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or + * expanded (the icons are in a row). This indicates whether the bubble bar is expanded. */ public boolean isExpanded() { return mBarView.isExpanded(); @@ -294,41 +711,60 @@ public class BubbleBarViewController { return mHiddenForNoBubbles; } + /** Returns maximum height of the bubble bar with the flyout view. */ + public int getBubbleBarWithFlyoutMaximumHeight() { + if (!hasBubbles() && !isAnimatingNewBubble()) return 0; + int bubbleBarTopOnHome = (int) (mBubbleStashController.getBubbleBarVerticalCenterForHome() + + mBarView.getBubbleBarCollapsedHeight() / 2 + mBarView.getArrowHeight()); + if (isAnimatingNewBubble()) { + if (mTaskbarStashController.isInApp() && mBubbleStashController.getHasHandleView()) { + // when animating a bubble in an app, the bubble bar will be higher than its + // position on home + float bubbleBarTopDistanceFromBottom = + -mBubbleStashController.getBubbleBarTranslationYForTaskbar() + + mBarView.getHeight(); + return (int) bubbleBarTopDistanceFromBottom + + mBubbleBarFlyoutController.getMaximumFlyoutHeight(); + } + return bubbleBarTopOnHome + mBubbleBarFlyoutController.getMaximumFlyoutHeight(); + } else { + return bubbleBarTopOnHome; + } + } + /** * Sets whether the bubble bar should be hidden because there are no bubbles. */ public void setHiddenForBubbles(boolean hidden) { if (mHiddenForNoBubbles != hidden) { mHiddenForNoBubbles = hidden; - updateVisibilityForStateChange(); if (hidden) { - mBarView.setAlpha(0); - mBarView.setExpanded(false); + mBarView.dismiss(() -> { + updateVisibilityForStateChange(); + mBarView.animateExpanded(false); + adjustTaskbarAndHotseatToBubbleBarState(/* isBubbleBarExpanded= */ false); + mActivity.bubbleBarVisibilityChanged(/* isVisible= */ false); + }); + } else { + updateVisibilityForStateChange(); + mActivity.bubbleBarVisibilityChanged(/* isVisible= */ true); } - mActivity.bubbleBarVisibilityChanged(!hidden); } } - /** - * Sets a callback that updates the selected bubble after the bubble bar - * collapses. - */ + /** Sets a callback that updates the selected bubble after the bubble bar collapses. */ public void setUpdateSelectedBubbleAfterCollapse( Consumer updateSelectedBubbleAfterCollapse) { mBarView.setUpdateSelectedBubbleAfterCollapse(updateSelectedBubbleAfterCollapse); } - /** - * Returns whether the bubble bar should be hidden because of the current sysui - * state. - */ + /** Returns whether the bubble bar should be hidden because of the current sysui state. */ boolean isHiddenForSysui() { return mHiddenForSysui; } /** - * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on - * lockscreen). + * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen). */ public void setHiddenForSysui(boolean hidden) { if (mHiddenForSysui != hidden) { @@ -337,40 +773,128 @@ public class BubbleBarViewController { } } - // TODO: (b/273592694) animate it - private void updateVisibilityForStateChange() { - if (!mHiddenForSysui && !mHiddenForNoBubbles) { - mBarView.setVisibility(VISIBLE); - } else { - mBarView.setVisibility(INVISIBLE); + /** Sets whether the bubble bar should be hidden due to stashed state */ + public void setHiddenForStashed(boolean hidden) { + if (mHiddenForStashed != hidden) { + mHiddenForStashed = hidden; + updateVisibilityForStateChange(); } } + private void updateVisibilityForStateChange() { + boolean hiddenForStashedAndNotAnimating = + mHiddenForStashed && !mBubbleBarViewAnimator.isAnimating(); + if (mHiddenForSysui || mHiddenForNoBubbles || hiddenForStashedAndNotAnimating) { + //TODO(b/404870188) this visibility change cause search view drag misbehavior + mBarView.setVisibility(INVISIBLE); + } else { + mBarView.setVisibility(VISIBLE); + } + } + + /** + * Returns the translation X of the transient taskbar according to the bubble bar location + * regardless of the current taskbar mode. + */ + public int getTransientTaskbarTranslationXForBubbleBar(BubbleBarLocation location) { + int taskbarShift = 0; + if (!isBubbleBarVisible() || mTaskbarViewPropertiesProvider == null) return taskbarShift; + Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds(); + if (taskbarViewBounds.isEmpty()) return taskbarShift; + int actualDistance = + getDistanceBetweenTransientTaskbarAndBubbleBar(location, taskbarViewBounds); + if (actualDistance < mBubbleBarTaskbarMinDistance) { + taskbarShift = mBubbleBarTaskbarMinDistance - actualDistance; + if (!location.isOnLeft(mBarView.isLayoutRtl())) { + taskbarShift = -taskbarShift; + } + } + return taskbarShift; + } + + private int getDistanceBetweenTransientTaskbarAndBubbleBar(BubbleBarLocation location, + Rect taskbarViewBounds) { + Resources res = mActivity.getResources(); + DeviceProfile transientDp = mActivity.getTransientTaskbarDeviceProfile(); + int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res, transientDp); + int transientPadding = getBubbleBarPaddingFromDeviceProfile(res, transientDp); + int transientWidthWithMargin = (int) (mBarView.getCollapsedWidthForIconSizeAndPadding( + transientIconSize, transientPadding) + mBarView.getHorizontalMargin()); + int distance; + if (location.isOnLeft(mBarView.isLayoutRtl())) { + distance = taskbarViewBounds.left - transientWidthWithMargin; + } else { + int displayWidth = res.getDisplayMetrics().widthPixels; + int bubbleBarLeft = displayWidth - transientWidthWithMargin; + distance = bubbleBarLeft - taskbarViewBounds.right; + } + return distance; + } + // // Modifying view related properties. // - private void updateBubbleBarIconSize(int newIconSize, boolean animate) { + /** Notifies controller of configuration change, so bubble bar can be adjusted */ + public void onBubbleBarConfigurationChanged(boolean animate) { + int newIconSize; + int newPadding; Resources res = mActivity.getResources(); + if (mBubbleStashController.isBubblesShowingOnHome() + || mBubbleStashController.isTransientTaskBar()) { + newIconSize = getBubbleBarIconSizeFromDeviceProfile(res); + newPadding = getBubbleBarPaddingFromDeviceProfile(res); + } else { + // the bubble bar is shown inside the persistent task bar, use preset sizes + newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar); + newPadding = res.getDimensionPixelSize( + R.dimen.bubblebar_icon_spacing_persistent_taskbar); + } + updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate); + } + + private int getBubbleBarIconSizeFromDeviceProfile(Resources res) { + return getBubbleBarIconSizeFromDeviceProfile(res, mActivity.getDeviceProfile()); + } + + private int getBubbleBarIconSizeFromDeviceProfile(Resources res, DeviceProfile deviceProfile) { DisplayMetrics dm = res.getDisplayMetrics(); float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_SMALL_DP, dm); + float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + APP_ICON_MEDIUM_DP, dm); + float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f; + int taskbarIconSize = deviceProfile.getTaskbarProfile().getIconSize(); + return taskbarIconSize <= smallMediumThreshold + ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) : + res.getDimensionPixelSize(R.dimen.bubblebar_icon_size); + + } + + private int getBubbleBarPaddingFromDeviceProfile(Resources res) { + return getBubbleBarPaddingFromDeviceProfile(res, mActivity.getDeviceProfile()); + } + + private int getBubbleBarPaddingFromDeviceProfile(Resources res, DeviceProfile deviceProfile) { + DisplayMetrics dm = res.getDisplayMetrics(); float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_MEDIUM_DP, dm); float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_LARGE_DP, dm); - float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f; float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f; - mIconSize = newIconSize <= smallMediumThreshold - ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) - : res.getDimensionPixelSize(R.dimen.bubblebar_icon_size); - float bubbleBarPadding = newIconSize >= mediumLargeThreshold - ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) - : res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); + return deviceProfile.getTaskbarProfile().getIconSize() >= mediumLargeThreshold + ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) : + res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); + } + + private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) { + if (mIconSize == iconSize && mBubbleBarPadding == padding) return; + mIconSize = iconSize; + mBubbleBarPadding = padding; if (animate) { - mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding); + mBarView.animateBubbleBarIconSize(iconSize, padding); } else { - mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding); + mBarView.setIconSizeAndPadding(iconSize, padding); } } @@ -382,20 +906,95 @@ public class BubbleBarViewController { updateTranslationY(); } + /** + * Sets the translation of the bubble bar during the stash animation. + */ + public void setTranslationYForStash(float transY) { + mBubbleBarStashTranslationY = transY; + updateTranslationY(); + } + private void updateTranslationY() { - mBarView.setTranslationY(mBubbleBarTranslationY.value - + mBubbleBarSwipeUpTranslationY); + mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY + + mBubbleBarStashTranslationY + getBubbleBarTranslationYForTaskbarPinning()); + } + + /** Computes translation y for taskbar pinning. */ + private float getBubbleBarTranslationYForTaskbarPinning() { + if (mTaskbarSharedState == null) return 0f; + float pinningProgress = mBubbleBarPinning.value; + if (mTaskbarSharedState.startTaskbarVariantIsTransient) { + return mapRange(pinningProgress, /* min = */ 0f, mTaskbarTranslationDelta); + } else { + return mapRange(pinningProgress, -mTaskbarTranslationDelta, /* max = */ 0f); + } + } + + private void setBubbleBarScaleAndPadding(float pinningProgress) { + Resources res = mActivity.getResources(); + // determine icon scale for pinning + int persistentIconSize = res.getDimensionPixelSize( + R.dimen.bubblebar_icon_size_persistent_taskbar); + int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res, + mActivity.getTransientTaskbarDeviceProfile()); + float pinningIconSize = mapRange(pinningProgress, transientIconSize, persistentIconSize); + + // determine bubble bar padding for pinning + int persistentPadding = res.getDimensionPixelSize( + R.dimen.bubblebar_icon_spacing_persistent_taskbar); + int transientPadding = getBubbleBarPaddingFromDeviceProfile(res, + mActivity.getTransientTaskbarDeviceProfile()); + float pinningPadding = mapRange(pinningProgress, transientPadding, persistentPadding); + mBarView.setIconSizeAndPaddingForPinning(pinningIconSize, pinningPadding); } /** - * Applies scale properties for the entire bubble bar. + * Calculates the vertical difference in the bubble bar positions for pinned and transient + * taskbar modes. */ - private void updateScale() { - float scale = mBubbleBarScale.value; + private int getBubbleBarTranslationDeltaForTaskbar(TaskbarActivityContext activity) { + Resources res = activity.getResources(); + int persistentBubbleSize = res + .getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar); + int persistentSpacingSize = res + .getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_persistent_taskbar); + int persistentBubbleBarSize = persistentBubbleSize + persistentSpacingSize * 2; + int persistentTaskbarHeight = + activity.getPersistentTaskbarDeviceProfile().getTaskbarProfile().getHeight(); + int persistentBubbleBarY = (persistentTaskbarHeight - persistentBubbleBarSize) / 2; + int transientBubbleBarY = + activity.getTransientTaskbarDeviceProfile().getTaskbarProfile().getBottomMargin(); + return transientBubbleBarY - persistentBubbleBarY; + } + + private void updateScaleX(float scale) { mBarView.setScaleX(scale); + } + + private void updateScaleY(float scale) { mBarView.setScaleY(scale); } + private void updateBackgroundScaleX(float scale) { + mBarView.setBackgroundScaleX(scale); + } + + private void updateBackgroundScaleY(float scale) { + mBarView.setBackgroundScaleY(scale); + } + + private void updateBubbleAlpha(float alpha) { + mBarView.setBubbleAlpha(alpha); + } + + private void updateBubbleOffsetY(float transY) { + mBarView.setBubbleOffsetY(transY); + } + + private void updateBackgroundAlpha(float alpha) { + mBarView.setBackgroundAlpha(alpha); + } + // // Manipulating the specific bubble views in the bar // @@ -403,60 +1002,159 @@ public class BubbleBarViewController { /** * Removes the provided bubble from the bubble bar. */ - public void removeBubble(BubbleBarItem b) { + public void removeBubble(BubbleBarBubble b) { if (b != null) { - mBarView.removeView(b.getView()); + mBarView.removeBubble(b.getView()); + b.getView().setController(null); } else { Log.w(TAG, "removeBubble, bubble was null!"); } } + /** Adds a new bubble and removes an old bubble at the same time. */ + public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble, BubbleBarBubble removedBubble, + @Nullable BubbleBarBubble bubbleToSelect, boolean isExpanding, + boolean suppressAnimation, boolean addOverflowToo) { + BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView(); + mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(), + bubbleToSelectView, addOverflowToo ? () -> showOverflow(true) : null); + addedBubble.getView().setOnClickListener(mBubbleClickListener); + addedBubble.getView().setController(mBubbleViewController); + removedBubble.getView().setController(null); + mBubbleDragController.setupBubbleView(addedBubble.getView()); + if (!suppressAnimation) { + animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false); + } + } + + /** Whether the overflow view is added to the bubble bar. */ + public boolean isOverflowAdded() { + return mOverflowAdded; + } + + /** Shows or hides the overflow view. */ + public void showOverflow(boolean showOverflow) { + if (mOverflowAdded == showOverflow) return; + mOverflowAdded = showOverflow; + if (mOverflowAdded) { + mBarView.addBubble(mOverflowBubble.getView(), /* suppressAnimation= */ true); + mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); + mOverflowBubble.getView().setController(mBubbleViewController); + } else { + mBarView.removeBubble(mOverflowBubble.getView()); + mOverflowBubble.getView().setOnClickListener(null); + mOverflowBubble.getView().setController(null); + } + } + + /** Adds the overflow view to the bubble bar while animating a view away. */ + public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble, + @Nullable BubbleBarBubble bubbleToSelect) { + if (mOverflowAdded) return; + mOverflowAdded = true; + BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView(); + mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(), + bubbleToSelectView, null /* onEndRunnable */); + mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); + mOverflowBubble.getView().setController(mBubbleViewController); + removedBubble.getView().setController(null); + } + + /** Removes the overflow view to the bubble bar while animating a view in. */ + public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble, + @Nullable BubbleBarBubble bubbleToSelect) { + if (!mOverflowAdded) return; + mOverflowAdded = false; + BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView(); + mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(), + bubbleToSelectView, null /* onEndRunnable */); + addedBubble.getView().setOnClickListener(mBubbleClickListener); + addedBubble.getView().setController(mBubbleViewController); + mOverflowBubble.getView().setController(null); + } + /** * Adds the provided bubble to the bubble bar. */ - public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation) { + public void addBubble(BubbleBarItem b, + boolean isExpanding, + boolean suppressAnimation, + @Nullable BubbleBarBubble bubbleToSelect + ) { if (b != null) { - mBarView.addBubble( - b.getView(), new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT)); - b.getView().setOnClickListener(mBubbleClickListener); - mBubbleDragController.setupBubbleView(b.getView()); - - if (b instanceof BubbleBarOverflow) { - return; - } + BubbleView bubbleToSelectView = + bubbleToSelect == null ? null : bubbleToSelect.getView(); + addBubbleView(b.getView(), suppressAnimation, bubbleToSelectView); if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) { - // the bubble bar and handle are initialized as part of the first bubble - // animation. + // the bubble bar and handle are initialized as part of the first bubble animation. // if the animation is suppressed, immediately stash or show the bubble bar to // ensure they've been initialized. - if (mTaskbarStashController.isInApp()) { + if (mTaskbarStashController.isInApp() + && mBubbleStashController.isTransientTaskBar() + && mTaskbarStashController.isStashed() + && !isExpanded()) { mBubbleStashController.stashBubbleBarImmediate(); } else { mBubbleStashController.showBubbleBarImmediate(); } return; } - animateBubbleNotification(bubble, isExpanding); + animateBubbleNotification(bubble, isExpanding, /* isUpdate= */ false); } else { Log.w(TAG, "addBubble, bubble was null!"); } } + private void addBubbleView(BubbleView bubbleView, boolean suppressAnimation, + BubbleView selectedBubbleView) { + mBarView.addBubble(bubbleView, selectedBubbleView, suppressAnimation); + bubbleView.setOnClickListener(mBubbleClickListener); + mBubbleDragController.setupBubbleView(bubbleView); + bubbleView.setController(mBubbleViewController); + } + + /** + * Restore a previous bubble that is stored in {@link TaskbarSharedState}. + */ + public void restoreBubble(BubbleBarItem b) { + addBubbleView(b.getView(), /* suppressAnimation= */ true, /* bubbleToSelectView= */ null); + } + /** Animates the bubble bar to notify the user about a bubble change. */ - public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding) { + public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding, + boolean isUpdate) { + // if we're not already animating another bubble, update the dot visibility. otherwise the + // the dot will be handled as part of the animation. + if (!mBubbleBarViewAnimator.isAnimating()) { + bubble.getView().updateDotVisibility( + /* animate= */ !mBubbleStashController.isStashed()); + } + // if we're expanded, don't animate the bubble bar. + if (isExpanded()) { + return; + } boolean isInApp = mTaskbarStashController.isInApp(); - // if this is the first bubble, animate to the initial state. one bubble is the - // overflow - // so check for at most 2 children. - if (mBarView.getChildCount() <= 2) { - mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding); + // if this is the first bubble, animate to the initial state. + if (mBarView.getBubbleChildCount() == 1 && !isUpdate) { + // If a drop target is visible and the first bubble is added, hide the empty drop target + if (mBarView.isShowingDropTarget()) { + mBubbleBarPinController.hideDropTarget(); + } + mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding, + mBarView.isShowingDropTarget()); + return; + } + // if we're not stashed or we're in persistent taskbar, animate for collapsed state. + boolean animateForCollapsed = !mBubbleStashController.isStashed() + || !mBubbleStashController.isTransientTaskBar(); + if (animateForCollapsed) { + mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding); return; } - // only animate the new bubble if we're in an app and not auto expanding - if (isInApp && !isExpanding && !isExpanded()) { - mBubbleBarViewAnimator.animateBubbleInForStashed(bubble); + if (isInApp && mBubbleStashController.getHasHandleView()) { + mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding); } } @@ -465,7 +1163,7 @@ public class BubbleBarViewController { */ public void reorderBubbles(List newOrder) { List viewList = newOrder.stream().filter(Objects::nonNull) - .map(BubbleBarBubble::getView).collect(toList()); + .map(BubbleBarBubble::getView).toList(); mBarView.reorder(viewList); } @@ -476,58 +1174,109 @@ public class BubbleBarViewController { mBarView.setSelectedBubble(newlySelected.getView()); } + /** @see #animateExpanded(boolean, boolean) */ + public void animateExpanded(boolean isExpanded) { + animateExpanded(isExpanded, /* maybeShowEdu= */ false); + } + /** - * Sets whether the bubble bar should be expanded (not unstashed, but have the - * contents - * within it expanded). This method notifies SystemUI that the bubble bar is - * expanded and - * showing a selected bubble. This method should ONLY be called from UI events - * originating - * from Launcher. + * Sets whether the bubble bar should be animated to expanded state (not unstashed, but have + * the contents within it expanded). This method notifies SystemUI that the bubble bar is + * expanded and showing a selected bubble. This method should ONLY be called from UI events + * originating from Launcher. + * + * @param isExpanded whether the bar should be expanded + * @param maybeShowEdu whether we should show the edu view before expanding */ - public void setExpanded(boolean isExpanded) { - if (isExpanded != mBarView.isExpanded()) { - mBarView.setExpanded(isExpanded); + public void animateExpanded(boolean isExpanded, boolean maybeShowEdu) { + // if we're trying to expand try showing the edu view instead + if (maybeShowEdu && isExpanded && !mBarView.isExpanded() && maybeShowEduView()) { + return; + } + if (!mBubbleBarPinning.isAnimating() && isExpanded != mBarView.isExpanded()) { + mBarView.animateExpanded(isExpanded); + adjustTaskbarAndHotseatToBubbleBarState(isExpanded); if (!isExpanded) { mSystemUiProxy.collapseBubbles(); } else { mBubbleBarController.showSelectedBubble(); - mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */, - false /* shouldBubblesFollow */); } } } /** - * Sets whether the bubble bar should be expanded. This method is used in - * response to UI events - * from SystemUI. + * Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in + * app or overview. */ - public void setExpandedFromSysui(boolean isExpanded) { - if (!isExpanded) { - mBubbleStashController.stashBubbleBar(); + private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) { + if (!mBubbleStashController.isBubblesShowingOnHome() + && !mBubbleStashController.isTransientTaskBar()) { + boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar(); + Animator taskbarAlphaAnimator = mTaskbarViewPropertiesProvider.getIconsAlpha() + .animateToValue(hideTaskbar ? 0 : 1); + taskbarAlphaAnimator.setDuration(hideTaskbar + ? TASKBAR_FADE_OUT_DURATION_MS : TASKBAR_FADE_IN_DURATION_MS); + if (!hideTaskbar) { + taskbarAlphaAnimator.setStartDelay(TASKBAR_FADE_IN_DELAY_MS); + } + taskbarAlphaAnimator.setInterpolator(Interpolators.LINEAR); + taskbarAlphaAnimator.start(); + } + } + + /** Return {@code true} if expanded bubble bar would intersect the taskbar. */ + public boolean isIntersectingTaskbar() { + if (mBarView.isExpanding() || mBarView.isExpanded()) { + Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds(); + return mBarView.getBubbleBarExpandedBounds().intersect(taskbarViewBounds); } else { - mBubbleStashController.showBubbleBar(true /* expand the bubbles */); + return false; } } /** - * Marks as should show education and shows the bubble bar in a collapsed state + * Sets whether the bubble bar should be expanded. This method is used in response to UI events + * from SystemUI. + */ + public void setExpandedFromSysui(boolean isExpanded, boolean animate) { + if (isNewBubbleAnimationRunningOrPending() && isExpanded) { + mBubbleBarViewAnimator.expandedWhileAnimating(); + return; + } + if (animate) { + if (!isExpanded) { + mBubbleStashController.stashBubbleBar(); + } else { + mBubbleStashController.showBubbleBar(true /* expand the bubbles */); + } + } else { + if (!isExpanded) { + mBubbleStashController.stashBubbleBarImmediate(); + } else { + mBubbleStashController.showBubbleBarImmediate(); + mBarView.setExpanded(true); + adjustTaskbarAndHotseatToBubbleBarState(true); + } + } + } + + /** + * Stores a request to show the education view for later processing when appropriate. + * + * @see #maybeShowEduView() */ public void prepareToShowEducation() { mShouldShowEducation = true; - mBubbleStashController.showBubbleBar(false /* expand the bubbles */); } /** * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI * that a bubble is being dragged to dismiss. - * + * * @param bubbleView dragged bubble view */ public void onBubbleDragStart(@NonNull BubbleView bubbleView) { - if (bubbleView.getBubble() == null) - return; + if (bubbleView.getBubble() == null) return; mSystemUiProxy.startBubbleDrag(bubbleView.getBubble().getKey()); mBarView.setDraggedBubble(bubbleView); @@ -537,7 +1286,13 @@ public class BubbleBarViewController { * Notifies SystemUI to expand the selected bubble when the bubble is released. */ public void onBubbleDragRelease(BubbleBarLocation location) { - mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen()); + mSystemUiProxy.stopBubbleDrag(location, mBarView.getTopToScreenBottom()); + } + + /** Handle given bubble being dismissed */ + public void onBubbleDismissed(BubbleView bubble) { + mBubbleBarController.onBubbleDismissed(bubble); + mBarView.removeBubble(bubble); } /** @@ -549,8 +1304,7 @@ public class BubbleBarViewController { /** Notifies that dragging the bubble bar ended. */ public void onBubbleBarDragEnd() { - // we may have changed the bubble bar translation Y value from the value it had - // at the + // we may have changed the bubble bar translation Y value from the value it had at the // beginning of the drag, so update the translation Y animator state mBubbleBarTranslationY.updateValue(mBarView.getTranslationY()); } @@ -558,8 +1312,7 @@ public class BubbleBarViewController { /** * Get translation for bubble bar when drag is released. * - * @see BubbleBarView#getBubbleBarDragReleaseTranslation(PointF, - * BubbleBarLocation) + * @see BubbleBarView#getBubbleBarDragReleaseTranslation(PointF, BubbleBarLocation) */ public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation, BubbleBarLocation location) { @@ -569,8 +1322,7 @@ public class BubbleBarViewController { /** * Get translation for bubble view when drag is released. * - * @see BubbleBarView#getDraggedBubbleReleaseTranslation(PointF, - * BubbleBarLocation) + * @see BubbleBarView#getDraggedBubbleReleaseTranslation(PointF, BubbleBarLocation) */ public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation, BubbleBarLocation location) { @@ -581,21 +1333,30 @@ public class BubbleBarViewController { } /** - * Called when bubble was dragged into the dismiss target. Notifies System - * - * @param bubble dismissed bubble item + * Notify SystemUI that the given bubble has been dismissed. */ - public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) { - mSystemUiProxy.dragBubbleToDismiss(bubble.getKey()); + public void notifySysUiBubbleDismissed(@NonNull BubbleBarItem bubble) { + mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis()); } /** - * Called when bubble stack was dragged into the dismiss target + * Called when bubble stack was dismissed */ - public void onDismissAllBubblesWhileDragging() { + public void onDismissAllBubbles() { mSystemUiProxy.removeAllBubbles(); } + /** Removes all existing bubble views */ + public void removeAllBubbles() { + mOverflowAdded = false; + mBarView.removeAllViews(); + } + + /** Returns the view index of the existing bubble */ + public int bubbleViewIndex(View bubbleView) { + return mBarView.indexOfChild(bubbleView); + } + /** * Set listener to be notified when bubble bar bounds have changed */ @@ -603,6 +1364,66 @@ public class BubbleBarViewController { mBoundsChangeListener = listener; } + /** Called when the controller is destroyed. */ + public void onDestroy() { + adjustTaskbarAndHotseatToBubbleBarState(/*isBubbleBarExpanded = */false); + } + + /** + * Removes the bubble from the bubble bar and notifies sysui that the bubble should move to + * full screen. + */ + public void moveDraggedBubbleToFullscreen(@NonNull BubbleView bubbleView, Point dropLocation) { + if (bubbleView.getBubble() == null) { + return; + } + String key = bubbleView.getBubble().getKey(); + mSystemUiProxy.moveDraggedBubbleToFullscreen(key, dropLocation); + onBubbleDismissed(bubbleView); + } + + /** + * Create an animator for showing or hiding bubbles when stashed state changes + * + * @param isStashed {@code true} when bubble bar should be stashed to the handle + */ + public Animator createRevealAnimatorForStashChange(boolean isStashed) { + Rect stashedHandleBounds = new Rect(); + mBubbleStashController.getHandleBounds(stashedHandleBounds); + int childCount = mBarView.getChildCount(); + float newChildWidth = (float) stashedHandleBounds.width() / childCount; + AnimatorSet animatorSet = new AnimatorSet(); + for (int i = 0; i < childCount; i++) { + BubbleView child = (BubbleView) mBarView.getChildAt(i); + animatorSet.play( + createRevealAnimForBubble(child, isStashed, stashedHandleBounds, + newChildWidth)); + } + return animatorSet; + } + + private Animator createRevealAnimForBubble(BubbleView bubbleView, boolean isStashed, + Rect stashedHandleBounds, float newWidth) { + Rect viewBounds = new Rect(0, 0, bubbleView.getWidth(), bubbleView.getHeight()); + + int viewCenterY = viewBounds.centerY(); + int halfHandleHeight = stashedHandleBounds.height() / 2; + int widthDelta = Math.max(0, (int) (viewBounds.width() - newWidth) / 2); + + Rect stashedViewBounds = new Rect( + viewBounds.left + widthDelta, + viewCenterY - halfHandleHeight, + viewBounds.right - widthDelta, + viewCenterY + halfHandleHeight + ); + + float viewRadius = 0f; // Use 0 to not clip the new message dot or the app icon + float stashedRadius = stashedViewBounds.height() / 2f; + + return new RoundedRectRevealOutlineProvider(viewRadius, stashedRadius, viewBounds, + stashedViewBounds).createRevealAnimator(bubbleView, !isStashed, 0); + } + /** * Listener to receive updates about bubble bar bounds changing */ @@ -610,4 +1431,36 @@ public class BubbleBarViewController { /** Called when bounds have changed */ void onBoundsChanged(); } + + /** Interface for getting the current timestamp. */ + interface TimeSource { + long currentTimeMillis(); + } + + /** Dumps the state of BubbleBarViewController. */ + public void dump(PrintWriter pw) { + pw.println("Bubble bar view controller state:"); + pw.println(" mHiddenForSysui: " + mHiddenForSysui); + pw.println(" mHiddenForNoBubbles: " + mHiddenForNoBubbles); + pw.println(" mHiddenForStashed: " + mHiddenForStashed); + pw.println(" mShouldShowEducation: " + mShouldShowEducation); + pw.println(" mBubbleBarTranslationY.value: " + mBubbleBarTranslationY.value); + pw.println(" mBubbleBarSwipeUpTranslationY: " + mBubbleBarSwipeUpTranslationY); + pw.println(" mOverflowAdded: " + mOverflowAdded); + if (mBarView != null) { + mBarView.dump(pw); + } else { + pw.println(" Bubble bar view is null!"); + } + } + + /** Interface for BubbleBarViewController to get the taskbar view properties. */ + public interface TaskbarViewPropertiesProvider { + + /** Returns the bounds of the taskbar. */ + Rect getTaskbarViewBounds(); + + /** Returns taskbar icons alpha */ + MultiPropertyFactory.MultiProperty getIconsAlpha(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java index 0b45bd902d..083829bd57 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java @@ -15,22 +15,38 @@ */ package com.android.launcher3.taskbar.bubbles; -import com.android.launcher3.taskbar.TaskbarControllers; -import com.android.launcher3.util.RunnableList; +import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_BUBBLE_BAR; -/** - * Hosts various bubble controllers to facilitate passing between one another. - */ +import android.graphics.Rect; +import android.view.View; + +import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.taskbar.TaskbarSharedState; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleBarLocationOnDemandListener; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; +import com.android.launcher3.util.MultiPropertyFactory; +import com.android.launcher3.util.RunnableList; +import com.android.quickstep.SystemUiProxy; +import com.android.wm.shell.shared.bubbles.DragZoneFactory; + +import java.io.PrintWriter; +import java.util.Optional; + +/** Hosts various bubble controllers to facilitate passing between one another. */ public class BubbleControllers { public final BubbleBarController bubbleBarController; public final BubbleBarViewController bubbleBarViewController; public final BubbleStashController bubbleStashController; - public final BubbleStashedHandleViewController bubbleStashedHandleViewController; + public final Optional bubbleStashedHandleViewController; public final BubbleDragController bubbleDragController; public final BubbleDismissController bubbleDismissController; public final BubbleBarPinController bubbleBarPinController; public final BubblePinController bubblePinController; + public final Optional bubbleBarSwipeController; + public final BubbleCreator bubbleCreator; + public final DragToBubbleController dragToBubbleController; private final RunnableList mPostInitRunnables = new RunnableList(); @@ -43,11 +59,14 @@ public class BubbleControllers { BubbleBarController bubbleBarController, BubbleBarViewController bubbleBarViewController, BubbleStashController bubbleStashController, - BubbleStashedHandleViewController bubbleStashedHandleViewController, + Optional bubbleStashedHandleViewController, BubbleDragController bubbleDragController, BubbleDismissController bubbleDismissController, BubbleBarPinController bubbleBarPinController, - BubblePinController bubblePinController) { + BubblePinController bubblePinController, + Optional bubbleBarSwipeController, + DragToBubbleController dragToBubbleController, + BubbleCreator bubbleCreator) { this.bubbleBarController = bubbleBarController; this.bubbleBarViewController = bubbleBarViewController; this.bubbleStashController = bubbleStashController; @@ -56,6 +75,9 @@ public class BubbleControllers { this.bubbleDismissController = bubbleDismissController; this.bubbleBarPinController = bubbleBarPinController; this.bubblePinController = bubblePinController; + this.bubbleBarSwipeController = bubbleBarSwipeController; + this.bubbleCreator = bubbleCreator; + this.dragToBubbleController = dragToBubbleController; } /** @@ -65,17 +87,63 @@ public class BubbleControllers { * were created * in constructors for now, as some controllers may still be waiting for init(). */ - public void init(TaskbarControllers taskbarControllers) { + public void init(TaskbarSharedState taskbarSharedState, TaskbarControllers taskbarControllers) { + BubbleBarLocationCompositeListener bubbleBarLocationListeners = + new BubbleBarLocationCompositeListener( + taskbarControllers.navbarButtonsViewController, + taskbarControllers.taskbarViewController, + new BubbleBarLocationOnDemandListener(() -> taskbarControllers.uiController) + ); bubbleBarController.init(this, - taskbarControllers.navbarButtonsViewController::isImeVisible); - bubbleBarViewController.init(taskbarControllers, this); - bubbleStashedHandleViewController.init(taskbarControllers, this); - bubbleStashController.init(taskbarControllers, this); - bubbleDragController.init(/* bubbleControllers = */ this); - bubbleDismissController.init(/* bubbleControllers = */ this); - bubbleBarPinController.init(this); - bubblePinController.init(this); + bubbleBarLocationListeners, + taskbarSharedState); + bubbleStashedHandleViewController.ifPresent( + controller -> controller.init(/* bubbleControllers = */ this)); + bubbleStashController.init( + taskbarControllers.taskbarInsetsController, + bubbleBarViewController, + bubbleStashedHandleViewController.orElse(null), + taskbarControllers::runAfterInit + ); + bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this, + new TaskbarViewPropertiesProvider() { + @Override + public Rect getTaskbarViewBounds() { + return taskbarControllers.taskbarViewController + .getTransientTaskbarIconLayoutBoundsInParent(); + } + @Override + public MultiPropertyFactory.MultiProperty getIconsAlpha() { + return taskbarControllers.taskbarViewController + .getTaskbarIconAlpha() + .get(ALPHA_INDEX_BUBBLE_BAR); + } + }); + bubbleDragController.init(/* bubbleControllers = */ this, bubbleBarLocationListeners); + bubbleDismissController.init(/* bubbleControllers = */ this); + bubbleBarPinController.init(this, bubbleBarLocationListeners); + bubblePinController.init(this); + bubbleBarSwipeController.ifPresent(c -> c.init(this)); + dragToBubbleController.init(bubbleBarViewController, + new DragZoneFactory.BubbleBarPropertiesProvider() { + @Override + public int getHeight() { + return (int) bubbleBarViewController.getBubbleBarCollapsedHeight(); + } + + @Override + public int getWidth() { + return (int) bubbleBarViewController.getBubbleBarCollapsedWidth(); + } + + @Override + public int getBottomPadding() { + return -(int) bubbleStashController.getBubbleBarTranslationY(); + } + }, + bubbleBarLocationListeners, + SystemUiProxy.INSTANCE.get(taskbarControllers.taskbarActivityContext)); mPostInitRunnables.executeAllAndDestroy(); } @@ -96,7 +164,13 @@ public class BubbleControllers { * Cleans up all controllers. */ public void onDestroy() { - bubbleStashedHandleViewController.onDestroy(); + bubbleStashedHandleViewController.ifPresent(BubbleStashedHandleViewController::onDestroy); bubbleBarController.onDestroy(); + bubbleBarViewController.onDestroy(); + } + + /** Dumps bubble controllers state. */ + public void dump(PrintWriter pw) { + bubbleBarViewController.dump(pw); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java new file mode 100644 index 0000000000..8b344cfd5f --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java @@ -0,0 +1,232 @@ +/* + * 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.launcher3.taskbar.bubbles; + +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER; + +import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA; +import static com.android.wm.shell.shared.bubbles.FlyoutDrawableLoader.loadFlyoutDrawable; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.UserHandle; +import android.util.Log; +import android.util.PathParser; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.R; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.BubbleIconFactory; +import com.android.launcher3.shortcuts.ShortcutRequest; +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage; +import com.android.wm.shell.shared.bubbles.BubbleInfo; +import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage; + +/** + * Loads the necessary info to populate / present a bubble (name, icon, shortcut). + */ +public class BubbleCreator { + + private static final String TAG = BubbleCreator.class.getSimpleName(); + + private final Context mContext; + private final LauncherApps mLauncherApps; + private final BubbleIconFactory mIconFactory; + + public BubbleCreator(Context context) { + mContext = context; + mLauncherApps = mContext.getSystemService(LauncherApps.class); + mIconFactory = new BubbleIconFactory(context, + context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size), + context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size), + context.getResources().getColor(R.color.important_conversation), + context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width)); + } + + /** + * Creates a BubbleBarBubble object, including the view if needed, and populates it with + * the info needed for presentation. + * + * @param context the context to use for inflation. + * @param info the info to use to populate the bubble. + * @param barView the parent view for the bubble (bubble is not added to the view). + * @param existingBubble if a bubble exists already, this object gets updated with the new + * info & returned (& any existing views are reused instead of inflating + * new ones. + */ + @Nullable + public BubbleBarBubble populateBubble(Context context, BubbleInfo info, ViewGroup barView, + @Nullable BubbleBarBubble existingBubble) { + String appName; + Bitmap badgeBitmap; + Bitmap bubbleBitmap; + Path dotPath; + int dotColor; + + boolean isImportantConvo = info.isImportantConversation(); + + ShortcutRequest.QueryResult result = new ShortcutRequest(context, + new UserHandle(info.getUserId())) + .forPackage(info.getPackageName(), info.getShortcutId()) + .query(FLAG_MATCH_DYNAMIC + | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER + | FLAG_MATCH_CACHED + | FLAG_GET_PERSONS_DATA); + + ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null; + if (shortcutInfo == null) { + Log.w(TAG, "No shortcutInfo found for bubble: " + info.getKey() + + " with shortcutId: " + info.getShortcutId()); + } + + ApplicationInfo appInfo; + try { + appInfo = mLauncherApps.getApplicationInfo( + info.getPackageName(), + 0, + new UserHandle(info.getUserId())); + } catch (PackageManager.NameNotFoundException e) { + // If we can't find package... don't think we should show the bubble. + Log.w(TAG, "Unable to find packageName: " + info.getPackageName()); + return null; + } + if (appInfo == null) { + Log.w(TAG, "Unable to find appInfo: " + info.getPackageName()); + return null; + } + PackageManager pm = context.getPackageManager(); + appName = String.valueOf(appInfo.loadLabel(pm)); + Drawable appIcon = appInfo.loadUnbadgedIcon(pm); + Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(info.getUserId())); + + // Badged bubble image + Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo, + info.getIcon()); + if (bubbleDrawable == null) { + // Default to app icon + bubbleDrawable = appIcon; + } + + BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo); + badgeBitmap = badgeBitmapInfo.icon; + + float[] bubbleBitmapScale = new float[1]; + bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); + + // Dot color & placement + Path iconPath = PathParser.createPathFromPathData( + context.getResources().getString( + com.android.internal.R.string.config_icon_mask)); + Matrix matrix = new Matrix(); + float scale = bubbleBitmapScale[0]; + float radius = BubbleView.DEFAULT_PATH_SIZE / 2f; + matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, + radius /* pivot y */); + iconPath.transform(matrix); + dotPath = iconPath; + dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, + Color.WHITE, WHITE_SCRIM_ALPHA / 255f); + + final BubbleBarFlyoutMessage flyoutMessage = + getFlyoutMessage(info.getParcelableFlyoutMessage()); + + if (existingBubble == null) { + LayoutInflater inflater = LayoutInflater.from(context); + BubbleView bubbleView = (BubbleView) inflater.inflate( + R.layout.bubblebar_item_view, barView, false /* attachToRoot */); + + BubbleBarBubble bubble = new BubbleBarBubble(info, bubbleView, + badgeBitmap, bubbleBitmap, dotColor, dotPath, appName, flyoutMessage); + bubbleView.setBubble(bubble); + return bubble; + } else { + // If we already have a bubble (so it already has an inflated view), update it. + existingBubble.setInfo(info); + existingBubble.setBadge(badgeBitmap); + existingBubble.setIcon(bubbleBitmap); + existingBubble.setDotColor(dotColor); + existingBubble.setDotPath(dotPath); + existingBubble.setAppName(appName); + existingBubble.setFlyoutMessage(flyoutMessage); + return existingBubble; + } + } + + @Nullable + private BubbleBarFlyoutMessage getFlyoutMessage( + @Nullable ParcelableFlyoutMessage parcelableFlyoutMessage) { + if (parcelableFlyoutMessage == null) { + return null; + } + String title = parcelableFlyoutMessage.getTitle(); + String message = parcelableFlyoutMessage.getMessage(); + return new BubbleBarFlyoutMessage( + loadFlyoutDrawable(parcelableFlyoutMessage.getIcon(), mContext), + title == null ? "" : title, + message == null ? "" : message); + } + + /** + * Creates the overflow view shown in the bubble bar. + * + * @param barView the parent view for the bubble (bubble is not added to the view). + */ + public BubbleBarOverflow createOverflow(ViewGroup barView) { + Bitmap bitmap = createOverflowBitmap(); + LayoutInflater inflater = LayoutInflater.from(mContext); + BubbleView bubbleView = (BubbleView) inflater.inflate( + R.layout.bubble_bar_overflow_button, barView, false /* attachToRoot */); + BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView); + bubbleView.setOverflow(overflow, bitmap); + return overflow; + } + + private Bitmap createOverflowBitmap() { + Drawable iconDrawable = mContext.getDrawable(R.drawable.bubble_ic_overflow_button); + + int overflowIconColor = mContext.getColor(R.color.materialColorOnPrimaryFixed); + int overflowBackgroundColor = mContext.getColor(R.color.materialColorPrimaryFixed); + + iconDrawable.setTint(overflowIconColor); + + int inset = mContext.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset); + Drawable foreground = new InsetDrawable(iconDrawable, inset); + Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor), + foreground); + + return mIconFactory.createBadgedIconBitmap(drawable).icon; + } + +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java index e3e95dc868..ca81d0c98f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java @@ -29,8 +29,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import com.android.launcher3.R; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarDragLayer; -import com.android.wm.shell.common.bubbles.DismissView; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.shared.bubbles.DismissView; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; /** @@ -144,10 +144,10 @@ public class BubbleDismissController { if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) { BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject(); if (bubbleView.getBubble() != null) { - mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble()); + mBubbleBarViewController.notifySysUiBubbleDismissed(bubbleView.getBubble()); } } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) { - mBubbleBarViewController.onDismissAllBubblesWhileDragging(); + mBubbleBarViewController.onDismissAllBubbles(); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt index a4797451b3..bd6f2c9168 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt @@ -18,7 +18,7 @@ package com.android.launcher3.taskbar.bubbles import com.android.launcher3.R -import com.android.wm.shell.common.bubbles.DismissView +import com.android.wm.shell.shared.bubbles.DismissView /** * Dismiss view is shared from WMShell. It requires setup with local resources. @@ -35,9 +35,10 @@ fun DismissView.setup() { iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size, bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin, floatingGradientHeightResId = R.dimen.bubblebar_dismiss_floating_gradient_height, - floatingGradientColorResId = R.color.system_neutral1_900, - backgroundResId = R.drawable.bg_bubble_dismiss_circle, - iconResId = R.drawable.ic_bubble_dismiss_white + floatingGradientColorResId = android.R.color.system_neutral1_900, + backgroundResId = R.drawable.floating_dismiss_background, + iconResId = R.drawable.floating_dismiss_ic_close, + applyMarginOverNavBarInset = false, ) ) } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java index 7b049c510d..b2c60a8d9b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java @@ -29,14 +29,14 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FloatPropertyCompat; import com.android.launcher3.R; -import com.android.wm.shell.common.bubbles.DismissCircleView; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.DismissCircleView; +import com.android.wm.shell.shared.bubbles.DismissView; +//import com.android.wm.shell.shared.animation.PhysicsAnimator; import app.lawnchair.animation.PhysicsAnimator; /** - * The animator performs the bubble animations while dragging and coordinates - * bubble and dismiss + * The animator performs the bubble animations while dragging and coordinates bubble and dismiss * view animations when it gets magnetized, released or dismissed. */ public class BubbleDragAnimator { @@ -46,11 +46,11 @@ public class BubbleDragAnimator { // 400f matches to MEDIUM_LOW spring stiffness private static final float TRANSLATION_SPRING_STIFFNESS = 400f; - private final PhysicsAnimator.SpringConfig mDefaultConfig = new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, - DAMPING_RATIO_LOW_BOUNCY); - private final PhysicsAnimator.SpringConfig mTranslationConfig = new PhysicsAnimator.SpringConfig( - TRANSLATION_SPRING_STIFFNESS, - DAMPING_RATIO_LOW_BOUNCY); + private final PhysicsAnimator.SpringConfig mDefaultConfig = + new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY); + private final PhysicsAnimator.SpringConfig mTranslationConfig = + new PhysicsAnimator.SpringConfig(TRANSLATION_SPRING_STIFFNESS, + DAMPING_RATIO_LOW_BOUNCY); @NonNull private final View mView; @NonNull @@ -114,8 +114,7 @@ public class BubbleDragAnimator { * * @param restingPosition the position to animate to * @param velocity the initial velocity to use for the spring animation - * @param endActions gets called when the animation completes or gets - * cancelled + * @param endActions gets called when the animation completes or gets cancelled */ public void animateToRestingState(@NonNull PointF restingPosition, @NonNull PointF velocity, @Nullable Runnable endActions) { @@ -131,7 +130,7 @@ public class BubbleDragAnimator { boolean wasFling, boolean canceled, float finalValue, float finalVelocity, boolean allRelevantPropertyAnimationsEnded) -> { if (canceled || allRelevantPropertyAnimationsEnded) { - resetAnimatedViews(restingPosition); + resetAnimatedViews(restingPosition, /* dismissed= */ false); if (endActions != null) { endActions.run(); } @@ -141,8 +140,7 @@ public class BubbleDragAnimator { } /** - * Animates the dragged view alongside the dismiss view when it gets captured in - * the dismiss + * Animates the dragged view alongside the dismiss view when it gets captured in the dismiss * target area. */ public void animateDismissCaptured() { @@ -163,8 +161,7 @@ public class BubbleDragAnimator { } /** - * Animates the dragged view alongside the dismiss view when it gets released - * from the dismiss + * Animates the dragged view alongside the dismiss view when it gets released from the dismiss * target area. */ public void animateDismissReleased() { @@ -185,13 +182,10 @@ public class BubbleDragAnimator { } /** - * Animates the dragged bubble dismiss when it's released in the dismiss target - * area. + * Animates the dragged bubble dismiss when it's released in the dismiss target area. * - * @param initialPosition the initial position to move the bubble too after - * animation finishes - * @param endActions gets called when the animation completes or gets - * cancelled + * @param initialPosition the initial position to move the bubble too after animation finishes + * @param endActions gets called when the animation completes or gets cancelled */ public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) { float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f; @@ -205,9 +199,8 @@ public class BubbleDragAnimator { boolean wasFling, boolean canceled, float finalValue, float finalVelocity, boolean allRelevantPropertyAnimationsEnded) -> { if (canceled || allRelevantPropertyAnimationsEnded) { - resetAnimatedViews(initialPosition); - if (endActions != null) - endActions.run(); + resetAnimatedViews(initialPosition, /* dismissed= */ true); + if (endActions != null) endActions.run(); } }) .start(); @@ -217,11 +210,14 @@ public class BubbleDragAnimator { * Reset the animated views to the initial state * * @param initialPosition position of the bubble + * @param dismissed whether the animated view was dismissed */ - private void resetAnimatedViews(@NonNull PointF initialPosition) { + private void resetAnimatedViews(@NonNull PointF initialPosition, boolean dismissed) { mView.setScaleX(1f); mView.setScaleY(1f); - mView.setAlpha(1f); + if (!dismissed) { + mView.setAlpha(1f); + } mView.setTranslationX(initialPosition.x); mView.setTranslationY(initialPosition.y); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java index efc747c725..7535d366cc 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java @@ -16,19 +16,33 @@ package com.android.launcher3.taskbar.bubbles; import android.annotation.SuppressLint; +import android.graphics.Point; import android.graphics.PointF; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.FloatPropertyCompat; import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener; +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.DeviceConfig; +import com.android.wm.shell.shared.bubbles.DragZone; +import com.android.wm.shell.shared.bubbles.DragZoneFactory; +import com.android.wm.shell.shared.bubbles.DragZoneFactory.BubbleBarPropertiesProvider; +import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker; +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker; +import com.android.wm.shell.shared.bubbles.DraggedObject; +import com.android.wm.shell.shared.bubbles.DropTargetManager; +import com.android.wm.shell.shared.bubbles.DropTargetManager.DragZoneChangedListener; /** * Controls bubble bar drag interactions. @@ -76,9 +90,54 @@ public class BubbleDragController { private BubbleDismissController mBubbleDismissController; private BubbleBarPinController mBubbleBarPinController; private BubblePinController mBubblePinController; + private BubbleBarLocationListener mBubbleBarLocationListener; + private final DropTargetManager mDropTargetManager; + private final DragZoneFactory mDragZoneFactory; + private final BubbleDragZoneChangedListener mBubbleDragZoneChangedListener; - public BubbleDragController(TaskbarActivityContext activity) { + private boolean mIsDragging; + + public BubbleDragController(TaskbarActivityContext activity, FrameLayout dropTargetParent) { mActivity = activity; + WindowManager windowManager = + mActivity.getApplicationContext().getSystemService(WindowManager.class); + DeviceConfig deviceConfig = + DeviceConfig.create(mActivity.getApplicationContext(), windowManager); + SplitScreenModeChecker splitScreenModeChecker = new SplitScreenModeChecker() { + @NonNull + @Override + public SplitScreenMode getSplitScreenMode() { + return SplitScreenMode.NONE; + } + }; + DesktopWindowModeChecker desktopWindowModeChecker = new DesktopWindowModeChecker() { + @Override + public boolean isSupported() { + return false; + } + }; + BubbleBarPropertiesProvider bubbleBarPropertiesProvider = + new BubbleBarPropertiesProvider() { + @Override + public int getHeight() { + return (int) mBubbleBarViewController.getBubbleBarCollapsedHeight(); + } + + @Override + public int getWidth() { + return (int) mBubbleBarViewController.getBubbleBarCollapsedWidth(); + } + + @Override + public int getBottomPadding() { + return (int) -mBubbleBarViewController.getBubbleBarTranslationY().value; + } + }; + mDragZoneFactory = new DragZoneFactory(mActivity.getApplicationContext(), deviceConfig, + splitScreenModeChecker, desktopWindowModeChecker, bubbleBarPropertiesProvider); + mBubbleDragZoneChangedListener = new BubbleDragZoneChangedListener(); + mDropTargetManager = new DropTargetManager(mActivity.getApplicationContext(), + dropTargetParent, mBubbleDragZoneChangedListener); } /** @@ -86,12 +145,14 @@ public class BubbleDragController { * Should be careful to only access things that were created in constructors for now, as some * controllers may still be waiting for init(). */ - public void init(@NonNull BubbleControllers bubbleControllers) { + public void init(@NonNull BubbleControllers bubbleControllers, + BubbleBarLocationListener bubbleBarLocationListener) { mBubbleBarController = bubbleControllers.bubbleBarController; mBubbleBarViewController = bubbleControllers.bubbleBarViewController; mBubbleDismissController = bubbleControllers.bubbleDismissController; mBubbleBarPinController = bubbleControllers.bubbleBarPinController; mBubblePinController = bubbleControllers.bubblePinController; + mBubbleBarLocationListener = bubbleBarLocationListener; mBubbleDismissController.setListener( stuck -> { if (stuck) { @@ -128,45 +189,88 @@ public class BubbleDragController { } }; + private BubbleBarLocation getBubbleBarLocationDuringDrag() { + return BubbleAnythingFlagHelper.enableBubbleToFullscreen() + ? mBubbleDragZoneChangedListener.mBubbleBarLocation + : mReleasedLocation; + } + @Override void onDragStart() { - mBubblePinController.setListener(mLocationChangeListener); mBubbleBarViewController.onBubbleDragStart(bubbleView); - mBubblePinController.onDragStart( - mBubbleBarViewController.getBubbleBarLocation().isOnLeft( - bubbleView.isLayoutRtl())); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + DraggedObject.Bubble draggedBubble = + new DraggedObject.Bubble( + mBubbleBarViewController.getBubbleBarLocation()); + mDropTargetManager.onDragStarted(draggedBubble, + mDragZoneFactory.createSortedDragZones(draggedBubble)); + } else { + mBubblePinController.setListener(mLocationChangeListener); + mBubblePinController.onDragStart( + mBubbleBarViewController.getBubbleBarLocation().isOnLeft( + bubbleView.isLayoutRtl())); + } } @Override protected void onDragUpdate(float x, float y, float newTx, float newTy) { bubbleView.setDragTranslationX(newTx); bubbleView.setTranslationY(newTy); - mBubblePinController.onDragUpdate(x, y); + if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mBubblePinController.onDragUpdate(x, y); + } } @Override protected void onDragRelease() { - mBubblePinController.onDragEnd(); - mBubbleBarViewController.onBubbleDragRelease(mReleasedLocation); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mDropTargetManager.onDragEnded(); + if (!mBubbleDragZoneChangedListener.isDraggedToFullscreen()) { + // TODO b/393173014: check for desktop window and split once they're + // implemented. this notifies wm shell that the dragged bubble was + // released so that we can show the expanded view. we only want to show it + // after releasing in a Bubble zone. But Split and Desktop Window aren't + // implemented yet, so we only check for full screen for now. + mBubbleBarViewController.onBubbleDragRelease( + getBubbleBarLocationDuringDrag()); + } + } else { + mBubblePinController.onDragEnd(); + mBubbleBarViewController.onBubbleDragRelease(getBubbleBarLocationDuringDrag()); + } } @Override protected void onDragDismiss() { - mBubblePinController.onDragEnd(); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mDropTargetManager.onDragEnded(); + } else { + mBubblePinController.onDragEnd(); + } + mBubbleBarViewController.onBubbleDismissed(bubbleView); mBubbleBarViewController.onBubbleDragEnd(); } @Override - void onDragEnd() { - mBubbleBarController.updateBubbleBarLocation(mReleasedLocation); + void onDragEnd(float x, float y) { + mBubbleBarController.updateBubbleBarLocation(getBubbleBarLocationDuringDrag(), + BubbleBarLocation.UpdateSource.DRAG_BUBBLE); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mDropTargetManager.onDragEnded(); + if (mBubbleDragZoneChangedListener.isDraggedToFullscreen()) { + mBubbleBarViewController.moveDraggedBubbleToFullscreen( + bubbleView, new Point((int) x, (int) y)); + } + } else { + mBubblePinController.setListener(null); + } mBubbleBarViewController.onBubbleDragEnd(); - mBubblePinController.setListener(null); } @Override protected PointF getRestingPosition() { return mBubbleBarViewController.getDraggedBubbleReleaseTranslation( - getInitialPosition(), mReleasedLocation); + getInitialPosition(), getBubbleBarLocationDuringDrag()); } }); } @@ -184,6 +288,12 @@ public class BubbleDragController { private final LocationChangeListener mLocationChangeListener = location -> mReleasedLocation = location; + private BubbleBarLocation getBubbleBarLocationDuringDrag() { + return BubbleAnythingFlagHelper.enableBubbleToFullscreen() + ? mBubbleDragZoneChangedListener.mBubbleBarLocation + : mReleasedLocation; + } + @Override protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) { if (bubbleBarView.isExpanded()) return false; @@ -192,53 +302,86 @@ public class BubbleDragController { @Override void onDragStart() { - mBubbleBarPinController.setListener(mLocationChangeListener); initialRelativePivot.set(bubbleBarView.getRelativePivotX(), bubbleBarView.getRelativePivotY()); // By default the bubble bar view pivot is in bottom right corner, while dragging // it should be centered in order to align it with the dismiss target view bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f); bubbleBarView.setIsDragging(true); - mBubbleBarPinController.onDragStart( - bubbleBarView.getBubbleBarLocation().isOnLeft(bubbleBarView.isLayoutRtl())); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + DraggedObject.BubbleBar draggedBubbleBar = new DraggedObject.BubbleBar( + mBubbleBarViewController.getBubbleBarLocation()); + mDropTargetManager.onDragStarted(draggedBubbleBar, + mDragZoneFactory.createSortedDragZones(draggedBubbleBar)); + } else { + mBubbleBarPinController.setListener(mLocationChangeListener); + mBubbleBarPinController.onDragStart( + bubbleBarView.getBubbleBarLocation().isOnLeft( + bubbleBarView.isLayoutRtl())); + } } @Override protected void onDragUpdate(float x, float y, float newTx, float newTy) { bubbleBarView.setTranslationX(newTx); bubbleBarView.setTranslationY(newTy); - mBubbleBarPinController.onDragUpdate(x, y); + if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mBubbleBarPinController.onDragUpdate(x, y); + } } @Override protected void onDragRelease() { - mBubbleBarPinController.onDragEnd(); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mDropTargetManager.onDragEnded(); + } else { + mBubbleBarPinController.onDragEnd(); + } } @Override protected void onDragDismiss() { - mBubbleBarPinController.onDragEnd(); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mDropTargetManager.onDragEnded(); + } else { + mBubbleBarPinController.onDragEnd(); + } } @Override - void onDragEnd() { + void onDragEnd(float x, float y) { // Make sure to update location as the first thing. Pivot update causes a relayout - mBubbleBarController.updateBubbleBarLocation(mReleasedLocation); + mBubbleBarController.updateBubbleBarLocation(getBubbleBarLocationDuringDrag(), + BubbleBarLocation.UpdateSource.DRAG_BAR); bubbleBarView.setIsDragging(false); // Restoring the initial pivot for the bubble bar view bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y); mBubbleBarViewController.onBubbleBarDragEnd(); - mBubbleBarPinController.setListener(null); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + mDropTargetManager.onDragEnded(); + } else { + mBubbleBarPinController.setListener(null); + } } @Override protected PointF getRestingPosition() { return mBubbleBarViewController.getBubbleBarDragReleaseTranslation( - getInitialPosition(), mReleasedLocation); + getInitialPosition(), getBubbleBarLocationDuringDrag()); } }); } + /** Whether there is an item being dragged or not. */ + public boolean isDragging() { + return mIsDragging; + } + + /** Sets whether something is being dragged or not. */ + public void setIsDragging(boolean isDragging) { + mIsDragging = isDragging; + } + /** * Bubble touch listener for handling a single bubble view or bubble bar view while dragging. * The dragging starts after "shorter" long click (the long click duration might change): @@ -284,7 +427,7 @@ public class BubbleDragController { private final PointF mTouchDownLocation = new PointF(); private final PointF mViewInitialPosition = new PointF(); private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); - private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2; + private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout(); private State mState = State.IDLE; private int mTouchSlop = -1; private BubbleDragAnimator mAnimator; @@ -305,7 +448,7 @@ public class BubbleDragController { /** * Called when the dragging interaction has ended and all the animations have completed */ - abstract void onDragEnd(); + abstract void onDragEnd(float x, float y); /** * Called when the dragged bubble is released outside of the dismiss target area and will @@ -435,6 +578,7 @@ public class BubbleDragController { private void startDragging(@NonNull View view) { onDragStart(); + BubbleDragController.this.setIsDragging(true); mActivity.setTaskbarWindowFullscreen(true); mAnimator = new BubbleDragAnimator(view); mAnimator.animateFocused(); @@ -444,17 +588,28 @@ public class BubbleDragController { private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy, float x, float y) { - if (mBubbleDismissController.handleTouchEvent(event)) return; + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + // notify drop target manager about the new drag location regardless of whether we + // are in the dismiss zone so that it can keep track of the current zone and update + // the drop target view + mDropTargetManager.onDragUpdated((int) x, (int) y); + } + if (mBubbleDismissController.handleTouchEvent(event)) { + // if we're dragging within the dismiss target, return immediately; the dragged + // object is manipulated by the dismiss target + return; + } final float newTx = mViewInitialPosition.x + dx; final float newTy = mViewInitialPosition.y + dy; onDragUpdate(x, y, newTx, newTy); } private void stopDragging(@NonNull View view, @NonNull MotionEvent event) { + BubbleDragController.this.setIsDragging(false); Runnable onComplete = () -> { mActivity.setTaskbarWindowFullscreen(false); cleanUp(view); - onDragEnd(); + onDragEnd(event.getRawX(), event.getRawY()); }; if (mBubbleDismissController.handleTouchEvent(event)) { @@ -462,8 +617,17 @@ public class BubbleDragController { mAnimator.animateDismiss(mViewInitialPosition, onComplete); } else { onDragRelease(); - mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(), - onComplete); + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + if (mBubbleDragZoneChangedListener.isDraggedToFullscreen()) { + onComplete.run(); + } else { + mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(), + onComplete); + } + } else { + mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(), + onComplete); + } } mBubbleDismissController.hideDismissView(); } @@ -503,4 +667,52 @@ public class BubbleDragController { return new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); } } + + private class BubbleDragZoneChangedListener implements DragZoneChangedListener { + + private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT; + private DragZone mDragZone; + + boolean isDraggedToFullscreen() { + return mDragZone instanceof DragZone.FullScreen; + } + + @Override + public void onInitialDragZoneSet(@Nullable DragZone dragZone) { + mDragZone = dragZone; + if (dragZone instanceof DragZone.Bubble.Left) { + mBubbleBarLocation = BubbleBarLocation.LEFT; + } else if (dragZone instanceof DragZone.Bubble.Right) { + mBubbleBarLocation = BubbleBarLocation.RIGHT; + } + } + + @Override + public void onDragZoneChanged(@NonNull DraggedObject draggedObject, @Nullable DragZone from, + @Nullable DragZone to) { + mDragZone = to; + if (to instanceof DragZone.Bubble.Left + && mBubbleBarLocation != BubbleBarLocation.LEFT) { + if (draggedObject instanceof DraggedObject.Bubble) { + // listener will be notified by BubbleBarController + mBubbleBarController.animateBubbleBarLocation(BubbleBarLocation.LEFT); + } else { + // otherwise notify listener manually + mBubbleBarLocationListener.onBubbleBarLocationAnimated(BubbleBarLocation.LEFT); + } + mBubbleBarLocation = BubbleBarLocation.LEFT; + } else if (to instanceof DragZone.Bubble.Right + && mBubbleBarLocation != BubbleBarLocation.RIGHT) { + if (draggedObject instanceof DraggedObject.Bubble) { + mBubbleBarController.animateBubbleBarLocation(BubbleBarLocation.RIGHT); + } else { + mBubbleBarLocationListener.onBubbleBarLocationAnimated(BubbleBarLocation.RIGHT); + } + mBubbleBarLocation = BubbleBarLocation.RIGHT; + } + } + + @Override + public void onDragEnded(@Nullable DragZone zone) {} + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt index a77e685d00..af1666fa7d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt @@ -27,8 +27,9 @@ import android.view.View import android.widget.FrameLayout import androidx.core.view.updateLayoutParams import com.android.launcher3.R -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** Controller to manage pinning bubble bar to left or right when dragging starts from a bubble */ class BubblePinController( diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java index 9d8186908c..7e2b139f2b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java @@ -21,6 +21,7 @@ import static android.view.View.VISIBLE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.content.res.Resources; import android.graphics.Outline; import android.graphics.Rect; @@ -28,47 +29,51 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewOutlineProvider; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.RevealOutlineAnimation; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.taskbar.StashedHandleView; import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.Executors; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; -import com.android.systemui.shared.navigationbar.RegionSamplingHelper; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; /** - * Handles properties/data collection, then passes the results to our stashed - * handle View to render. + * Handles properties/data collection, then passes the results to our stashed handle View to render. */ public class BubbleStashedHandleViewController { private final TaskbarActivityContext mActivity; private final StashedHandleView mStashedHandleView; private final MultiValueAlpha mStashedHandleAlpha; + private float mTranslationForSwipeY; + private float mTranslationForStashY; // Initialized in init. private BubbleBarViewController mBarViewController; private BubbleStashController mBubbleStashController; private RegionSamplingHelper mRegionSamplingHelper; - private int mBarSize; - private int mStashedTaskbarHeight; + // Height of the area for the stash handle. Handle will be drawn in the center of this. + // This is also the area where touch is handled on the handle. + private int mStashedBubbleBarHeight; private int mStashedHandleWidth; private int mStashedHandleHeight; - // The bounds we want to clip to in the settled state when showing the stashed - // handle. + // The bounds of the stashed handle in settled state. private final Rect mStashedHandleBounds = new Rect(); + private float mStashedHandleRadius; - // When the reveal animation is cancelled, we can assume it's about to create a - // new animation, + // When the reveal animation is cancelled, we can assume it's about to create a new animation, // which should start off at the same point the cancelled one left off. private float mStartProgressForNextRevealAnim; - private boolean mWasLastRevealAnimReversed; + // Use a nullable boolean to handle initial case where the last animation direction is not known + @Nullable + private Boolean mWasLastRevealAnimReversed = null; // XXX: if there are more of these maybe do state flags instead private boolean mHiddenForSysui; @@ -82,30 +87,36 @@ public class BubbleStashedHandleViewController { mStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1); } - public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { + /** Initialize controller. */ + public void init(BubbleControllers bubbleControllers) { mBarViewController = bubbleControllers.bubbleBarViewController; mBubbleStashController = bubbleControllers.bubbleStashController; + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); Resources resources = mActivity.getResources(); mStashedHandleHeight = resources.getDimensionPixelSize( R.dimen.bubblebar_stashed_handle_height); mStashedHandleWidth = resources.getDimensionPixelSize( R.dimen.bubblebar_stashed_handle_width); - mBarSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size); - final int bottomMargin = resources.getDimensionPixelSize( - R.dimen.transient_taskbar_bottom_margin); - mStashedHandleView.getLayoutParams().height = mBarSize + bottomMargin; + int barSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size); + // Use the max translation for bubble bar whether it is on the home screen or in app. + // Use values directly from device profile to avoid referencing other bubble controllers + // during init flow. + int maxTy = Math.max(deviceProfile.hotseatBarBottomSpacePx, + deviceProfile.getTaskbarProfile().getBottomMargin()); + // Adjust handle view size to accommodate the handle morphing into the bubble bar + mStashedHandleView.getLayoutParams().height = barSize + maxTy; mStashedHandleAlpha.get(0).setValue(0); - mStashedTaskbarHeight = resources.getDimensionPixelSize( + mStashedBubbleBarHeight = resources.getDimensionPixelSize( R.dimen.bubblebar_stashed_size); mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - float stashedHandleRadius = view.getHeight() / 2f; - outline.setRoundRect(mStashedHandleBounds, stashedHandleRadius); + mStashedHandleRadius = view.getHeight() / 2f; + outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius); } }); @@ -120,10 +131,10 @@ public class BubbleStashedHandleViewController { public Rect getSampledRegion(View sampledView) { return mStashedHandleView.getSampledRegion(); } - }, Executors.UI_HELPER_EXECUTOR); + }, Executors.MAIN_EXECUTOR, Executors.UI_HELPER_EXECUTOR); - mStashedHandleView.addOnLayoutChangeListener( - (view, i, i1, i2, i3, i4, i5, i6, i7) -> updateBounds(mBarViewController.getBubbleBarLocation())); + mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> + updateBounds(mBarViewController.getBubbleBarLocation())); } /** Returns the [PhysicsAnimator] for the stashed handle view. */ @@ -132,30 +143,27 @@ public class BubbleStashedHandleViewController { } private void updateBounds(BubbleBarLocation bubbleBarLocation) { - // As more bubbles get added, the icon bounds become larger. To ensure a - // consistent + // As more bubbles get added, the icon bounds become larger. To ensure a consistent // handle bar position, we pin it to the edge of the screen. - final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2; + final int stashedCenterY = mStashedHandleView.getHeight() - mStashedBubbleBarHeight / 2; + final int stashedCenterX; if (bubbleBarLocation.isOnLeft(mStashedHandleView.isLayoutRtl())) { final int left = mBarViewController.getHorizontalMargin(); - mStashedHandleBounds.set( - left, - stashedCenterY - mStashedHandleHeight / 2, - left + mStashedHandleWidth, - stashedCenterY + mStashedHandleHeight / 2); - mStashedHandleView.setPivotX(0); + stashedCenterX = left + mStashedHandleWidth / 2; } else { - final int right = mActivity.getDeviceProfile().widthPx - mBarViewController.getHorizontalMargin(); - mStashedHandleBounds.set( - right - mStashedHandleWidth, - stashedCenterY - mStashedHandleHeight / 2, - right, - stashedCenterY + mStashedHandleHeight / 2); - mStashedHandleView.setPivotX(mStashedHandleView.getWidth()); + final int right = + mStashedHandleView.getRight() - mBarViewController.getHorizontalMargin(); + stashedCenterX = right - mStashedHandleWidth / 2; } - + mStashedHandleBounds.set( + stashedCenterX - mStashedHandleWidth / 2, + stashedCenterY - mStashedHandleHeight / 2, + stashedCenterX + mStashedHandleWidth / 2, + stashedCenterY + mStashedHandleHeight / 2 + ); mStashedHandleView.updateSampledRegion(mStashedHandleBounds); - mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f); + mStashedHandleView.setPivotX(stashedCenterX); + mStashedHandleView.setPivotY(stashedCenterY); } public void onDestroy() { @@ -163,6 +171,13 @@ public class BubbleStashedHandleViewController { mRegionSamplingHelper = null; } + /** + * Returns the width of the stashed handle. + */ + public int getStashedWidth() { + return mStashedHandleWidth; + } + /** * Returns the height of the stashed handle. */ @@ -171,16 +186,14 @@ public class BubbleStashedHandleViewController { } /** - * Returns the height when the bubble bar is unstashed (so the height of the - * bubble bar). + * Returns bounds of the stashed handle view */ - public int getUnstashedHeight() { - return mBarSize; + public void getBounds(Rect bounds) { + bounds.set(mStashedHandleBounds); } /** - * Called when system ui state changes. Bubbles don't show when the device is - * locked. + * Called when system ui state changes. Bubbles don't show when the device is locked. */ public void setHiddenForSysui(boolean hidden) { if (mHiddenForSysui != hidden) { @@ -190,8 +203,7 @@ public class BubbleStashedHandleViewController { } /** - * Called when the handle should be hidden (or shown) because there are no - * bubbles + * Called when the handle should be hidden (or shown) because there are no bubbles * (or 1+ bubbles). */ public void setHiddenForBubbles(boolean hidden) { @@ -202,8 +214,7 @@ public class BubbleStashedHandleViewController { } /** - * Called when the home button is enabled / disabled. Bubbles don't show if home - * is disabled. + * Called when the home button is enabled / disabled. Bubbles don't show if home is disabled. */ // TODO: is this needed for bubbles? public void setIsHomeButtonDisabled(boolean homeDisabled) { @@ -223,8 +234,7 @@ public class BubbleStashedHandleViewController { } /** - * Called when bubble bar is stash state changes so that updates to the stashed - * handle color + * Called when bubble bar is stash state changes so that updates to the stashed handle color * can be started or stopped. */ public void onIsStashedChanged() { @@ -249,37 +259,56 @@ public class BubbleStashedHandleViewController { * Sets the translation of the stashed handle during the swipe up gesture. */ public void setTranslationYForSwipe(float transY) { - mStashedHandleView.setTranslationY(transY); + mTranslationForSwipeY = transY; + updateTranslationY(); } /** - * Used by {@link BubbleStashController} to animate the handle when stashing or - * un stashing. + * Sets the translation of the stashed handle during the spring on stash animation. + */ + public void setTranslationYForStash(float transY) { + mTranslationForStashY = transY; + updateTranslationY(); + } + + /** Sets translation X for stash handle. */ + public void setTranslationX(float translationX) { + mStashedHandleView.setTranslationX(translationX); + } + + private void updateTranslationY() { + mStashedHandleView.setTranslationY(mTranslationForSwipeY + mTranslationForStashY); + } + + /** Returns the translation of the stashed handle. */ + public float getTranslationY() { + return mStashedHandleView.getTranslationY(); + } + + /** + * Used by {@link BubbleStashController} to animate the handle when stashing or un stashing. */ public MultiPropertyFactory getStashedHandleAlpha() { return mStashedHandleAlpha; } /** - * Creates and returns an Animator that updates the stashed handle shape and - * size. - * When stashed, the shape is a thin rounded pill. When unstashed, the shape - * morphs into + * Creates and returns an Animator that updates the stashed handle shape and size. + * When stashed, the shape is a thin rounded pill. When unstashed, the shape morphs into * the size of where the bubble bar icons will be. */ public Animator createRevealAnimToIsStashed(boolean isStashed) { - Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds()); + Rect bubbleBarBounds = getLocalBubbleBarBounds(); - // Account for the full visual height of the bubble bar - int heightDiff = (mBarSize - bubbleBarBounds.height()) / 2; - bubbleBarBounds.top -= heightDiff; - bubbleBarBounds.bottom += heightDiff; - float stashedHandleRadius = mStashedHandleView.getHeight() / 2f; + float bubbleBarRadius = bubbleBarBounds.height() / 2f; final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider( - stashedHandleRadius, stashedHandleRadius, bubbleBarBounds, mStashedHandleBounds); + bubbleBarRadius, mStashedHandleRadius, bubbleBarBounds, mStashedHandleBounds); boolean isReversed = !isStashed; - boolean changingDirection = mWasLastRevealAnimReversed != isReversed; + // We are only changing direction when mWasLastRevealAnimReversed is set at least once + boolean changingDirection = + mWasLastRevealAnimReversed != null && mWasLastRevealAnimReversed != isReversed; + mWasLastRevealAnimReversed = isReversed; if (changingDirection) { mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim; @@ -297,25 +326,32 @@ public class BubbleStashedHandleViewController { } /** - * Checks that the stash handle is visible and that the motion event is within - * bounds. + * Get bounds for the bubble bar in the space of the handle view */ + private Rect getLocalBubbleBarBounds() { + // Position the bubble bar bounds to the space of handle view + Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds()); + // Start by moving bubble bar bounds to the bottom of handle view + int height = bubbleBarBounds.height(); + bubbleBarBounds.bottom = mStashedHandleView.getHeight(); + bubbleBarBounds.top = bubbleBarBounds.bottom - height; + // Then apply translation that is applied to the bubble bar + bubbleBarBounds.offset(0, (int) mBubbleStashController.getBubbleBarTranslationY()); + return bubbleBarBounds; + } + + /** Checks that the stash handle is visible and that the motion event is within bounds. */ public boolean isEventOverHandle(MotionEvent ev) { if (mStashedHandleView.getVisibility() != VISIBLE) { return false; } - // the bounds of the handle only include the visible part, so we check that the - // Y coordinate - // is anywhere within the stashed taskbar height. - int top = mActivity.getDeviceProfile().heightPx - mStashedTaskbarHeight; - - return (int) ev.getRawY() >= top && containsX((int) ev.getRawX()); - } - - /** Checks if the given x coordinate is within the stashed handle bounds. */ - public boolean containsX(int x) { - return x >= mStashedHandleBounds.left && x <= mStashedHandleBounds.right; + // the bounds of the handle only include the visible part, so we check that the Y coordinate + // is anywhere within the stashed height of bubble bar (same as taskbar stashed height). + final int top = mActivity.getDeviceProfile().getDeviceProperties().getHeightPx() - mStashedBubbleBarHeight; + final float x = ev.getRawX(); + return ev.getRawY() >= top && x >= mStashedHandleBounds.left + && x <= mStashedHandleBounds.right; } /** Set a bubble bar location */ diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java index a475fc047f..75193dd303 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java @@ -16,24 +16,30 @@ package com.android.launcher3.taskbar.bubbles; import android.annotation.Nullable; +import android.app.Notification; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Outline; +import android.graphics.Color; +import android.graphics.Path; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.R; import com.android.launcher3.icons.DotRenderer; -import com.android.launcher3.icons.IconNormalizer; -import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleInfo; + import com.patrykmichalik.opto.core.PreferenceExtensionsKt; import app.lawnchair.preferences2.PreferenceManager2; import app.lawnchair.theme.color.ColorOption; @@ -43,34 +49,18 @@ import java.util.EnumSet; // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share. /** - * View that displays a bubble icon, along with an app badge on either the left - * or + * View that displays a bubble icon, along with an app badge on either the left or * right side of the view. */ public class BubbleView extends ConstraintLayout { public static final int DEFAULT_PATH_SIZE = 100; - - /** - * Flags that suppress the visibility of the 'new' dot or the app badge, for one - * reason or - * another. If any of these flags are set, the dot will not be shown. - * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown. - */ - enum SuppressionFlag { - // TODO: (b/277815200) implement flyout - // Suppressed because the flyout is visible - it will morph into the dot via - // animation. - FLYOUT_VISIBLE, - // Suppressed because this bubble is behind others in the collapsed stack. - BEHIND_STACK, - } - - private final EnumSet mSuppressionFlags = EnumSet.noneOf(SuppressionFlag.class); + /** Duration for animating the scale of the dot and badge. */ + private static final int SCALE_ANIMATION_DURATION_MS = 200; private final ImageView mBubbleIcon; private final ImageView mAppIcon; - private final int mBubbleSize; + private int mBubbleSize; private float mDragTranslationX; private float mOffsetX; @@ -86,12 +76,22 @@ public class BubbleView extends ConstraintLayout { private float mAnimatingToDotScale; // The current scale value of the dot private float mDotScale; + private boolean mDotSuppressedForBubbleUpdate = false; // TODO: (b/273310265) handle RTL // Whether the bubbles are positioned on the left or right side of the screen private boolean mOnLeft = false; private BubbleBarItem mBubble; + private boolean mIsOverflow; + + private Bitmap mIcon; + + @Nullable + private Controller mController; + + @Nullable + private BubbleBarBubbleIconsFactory mIconFactory = null; PreferenceManager2 preferenceManager2; @@ -114,8 +114,6 @@ public class BubbleView extends ConstraintLayout { setLayoutDirection(LAYOUT_DIRECTION_LTR); LayoutInflater.from(context).inflate(R.layout.bubble_view, this); - - mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size); mBubbleIcon = findViewById(R.id.icon_view); mAppIcon = findViewById(R.id.app_icon_view); @@ -125,24 +123,26 @@ public class BubbleView extends ConstraintLayout { setFocusable(true); setClickable(true); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - BubbleView.this.getOutline(outline); - } - }); + + // We manage the shadow ourselves when creating the bitmap + setOutlineAmbientShadowColor(Color.TRANSPARENT); + setOutlineSpotShadowColor(Color.TRANSPARENT); } - private void getOutline(Outline outline) { - final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize); - final int inset = (mBubbleSize - normalizedSize) / 2; - outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); + private void updateBubbleSizeAndDotRender() { + int updatedBubbleSize = Math.min(getWidth(), getHeight()); + if (updatedBubbleSize == mBubbleSize) return; + mBubbleSize = updatedBubbleSize; + mIconFactory = new BubbleBarBubbleIconsFactory(mContext, mBubbleSize); + updateBubbleIcon(); + if (mBubble == null || mBubble instanceof BubbleBarOverflow) return; + Path dotPath = ((BubbleBarBubble) mBubble).getDotPath(); + mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE); } /** * Set translation-x while this bubble is being dragged. - * Translation applied to the view is a sum of {@code translationX} and offset - * defined by + * Translation applied to the view is a sum of {@code translationX} and offset defined by * {@link #setOffsetX(float)}. */ public void setDragTranslationX(float translationX) { @@ -159,11 +159,9 @@ public class BubbleView extends ConstraintLayout { /** * Set offset on x-axis while dragging. - * Used to counter parent translation in order to keep the dragged view at the - * current position + * Used to counter parent translation in order to keep the dragged view at the current position * on screen. - * Translation applied to the view is a sum of {@code offsetX} and translation - * defined by + * Translation applied to the view is a sum of {@code offsetX} and translation defined by * {@link #setDragTranslationX(float)} */ public void setOffsetX(float offsetX) { @@ -175,6 +173,12 @@ public class BubbleView extends ConstraintLayout { setTranslationX(mDragTranslationX + mOffsetX); } + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateBubbleSizeAndDotRender(); + } + @Override public void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); @@ -185,19 +189,78 @@ public class BubbleView extends ConstraintLayout { getDrawingRect(mTempBounds); - mDrawParams.appColor = mDotColor; + mDrawParams.dotColor = mDotColor; mDrawParams.iconBounds = mTempBounds; mDrawParams.leftAlign = mOnLeft; mDrawParams.scale = mDotScale; - mDotRenderer.draw(canvas, mDrawParams, -1); + mDotRenderer.draw(canvas, mDrawParams); + } + + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); + info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE); + if (mBubble instanceof BubbleBarBubble) { + info.addAction(AccessibilityNodeInfo.ACTION_DISMISS); + } + if (mController != null) { + if (mController.getBubbleBarLocation().isOnLeft(isLayoutRtl())) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right, + getResources().getString(R.string.bubble_bar_action_move_right))); + } else { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left, + getResources().getString(R.string.bubble_bar_action_move_left))); + } + } + } + + @Override + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { + return true; + } + if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { + if (mController != null) { + mController.collapse(); + } + return true; + } + if (action == AccessibilityNodeInfo.ACTION_DISMISS) { + if (mController != null) { + mController.dismiss(this); + } + return true; + } + if (action == R.id.action_move_left) { + if (mController != null) { + mController.updateBubbleBarLocation(BubbleBarLocation.LEFT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE); + } + } + if (action == R.id.action_move_right) { + if (mController != null) { + mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT, + BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE); + } + } + return false; + } + + void setController(@Nullable Controller controller) { + mController = controller; } /** Sets the bubble being rendered in this view. */ public void setBubble(BubbleBarBubble bubble) { mBubble = bubble; - mBubbleIcon.setImageBitmap(bubble.getIcon()); - mAppIcon.setImageBitmap(bubble.getBadge()); + mIcon = bubble.getIcon(); + updateBubbleIcon(); + if (bubble.getInfo().showAppBadge()) { + mAppIcon.setImageBitmap(bubble.getBadge()); + } else { + mAppIcon.setVisibility(GONE); + } mDotColor = bubble.getDotColor(); ColorOption dotColorOption = PreferenceExtensionsKt.firstBlocking(preferenceManager2.getNotificationDotColor()); int dotColor = dotColorOption.getColorPreferenceEntry().getLightColor().invoke(getContext()); @@ -218,32 +281,53 @@ public class BubbleView extends ConstraintLayout { setContentDescription(contentDesc); } + private void updateBubbleIcon() { + Bitmap icon = null; + if (mIcon != null) { + icon = mIcon; + if (mIconFactory != null) { + BitmapDrawable iconDrawable = new BitmapDrawable(getResources(), icon); + icon = mIconFactory.createShadowedIconBitmap(iconDrawable, /* scale = */ 1f); + } + } + mBubbleIcon.setImageBitmap(icon); + } + /** - * Sets that this bubble represents the overflow. The overflow appears in the - * list of bubbles - * but does not represent app content, instead it shows recent bubbles that - * couldn't fit into - * the list of bubbles. It doesn't show an app icon because it is part of system - * UI / doesn't + * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles + * but does not represent app content, instead it shows recent bubbles that couldn't fit into + * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't * come from an app. */ public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) { mBubble = overflow; - mBubbleIcon.setImageBitmap(bitmap); + mIsOverflow = true; + mIcon = bitmap; + updateBubbleIcon(); mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description)); } + /** Whether this view represents the overflow button. */ + public boolean isOverflow() { + return mIsOverflow; + } + /** Returns the bubble being rendered in this view. */ @Nullable public BubbleBarItem getBubble() { return mBubble; } - void updateDotVisibility(boolean animate) { - final float targetScale = shouldDrawDot() ? 1f : 0f; + /** Updates the dot visibility if it's not suppressed based on whether it has unseen content. */ + public void updateDotVisibility(boolean animate) { + if (mDotSuppressedForBubbleUpdate) { + // if the dot is suppressed for an update, there's nothing to do + return; + } + final float targetScale = hasUnseenContent() ? 1f : 0f; if (animate) { - animateDotScale(); + animateDotScale(targetScale); } else { mDotScale = targetScale; mAnimatingToDotScale = targetScale; @@ -251,77 +335,132 @@ public class BubbleView extends ConstraintLayout { } } - void updateBadgeVisibility() { - if (mBubble instanceof BubbleBarOverflow) { - // The overflow bubble does not have a badge, so just bail. + void setBadgeScale(float fraction) { + if (hasBadge()) { + mAppIcon.setScaleX(fraction); + mAppIcon.setScaleY(fraction); + } + } + + void showBadge() { + animateBadgeScale(1); + } + + void hideBadge() { + animateBadgeScale(0); + } + + private boolean hasBadge() { + return mAppIcon.getVisibility() == VISIBLE; + } + + private void animateBadgeScale(float scale) { + if (!hasBadge()) { return; } - BubbleBarBubble bubble = (BubbleBarBubble) mBubble; - Bitmap appBadgeBitmap = bubble.getBadge(); - int translationX = mOnLeft - ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth()) - : 0; - mAppIcon.setTranslationX(translationX); - mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE); + mAppIcon.clearAnimation(); + mAppIcon.animate() + .setDuration(SCALE_ANIMATION_DURATION_MS) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .scaleX(scale) + .scaleY(scale) + .start(); } - /** Sets whether this bubble is in the stack & not the first bubble. **/ - void setBehindStack(boolean behindStack, boolean animate) { - if (behindStack) { - mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK); - } else { - mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK); - } - updateDotVisibility(animate); - updateBadgeVisibility(); + /** Suppresses drawing the dot due to an update for this bubble. */ + public void suppressDotForBubbleUpdate() { + mDotSuppressedForBubbleUpdate = true; + setDotScale(0); } - /** Whether this bubble is in the stack & not the first bubble. **/ - boolean isBehindStack() { - return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK); + /** + * Unsuppresses the dot after the bubble update finished animating. + * + * @param animate whether or not to animate the dot back in + */ + public void unsuppressDotForBubbleUpdate(boolean animate) { + mDotSuppressedForBubbleUpdate = false; + showDotIfNeeded(animate); } - /** Whether the dot indicating unseen content in a bubble should be shown. */ - private boolean shouldDrawDot() { - boolean bubbleHasUnseenContent = mBubble != null + boolean hasUnseenContent() { + return mBubble != null && mBubble instanceof BubbleBarBubble - && mSuppressionFlags.isEmpty() && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed(); - - // Always render the dot if it's animating, since it could be animating out. - // Otherwise, show - // it if the bubble wants to show it, and we aren't suppressing it. - return bubbleHasUnseenContent || mDotIsAnimating; } - /** How big the dot should be, fraction from 0 to 1. */ + /** + * Used to determine if we can skip drawing frames. + * + *

Generally we should draw the dot when it is requested to be shown and there is unseen + * content. But when the dot is removed, we still want to draw frames so that it can be scaled + * out. + */ + private boolean shouldDrawDot() { + // if there's no dot there's nothing to draw, unless the dot was removed and we're in the + // middle of removing it + return hasUnseenContent() || mDotIsAnimating; + } + + /** Updates the dot scale to the specified fraction from 0 to 1. */ private void setDotScale(float fraction) { + if (!shouldDrawDot()) { + return; + } mDotScale = fraction; invalidate(); } - /** - * Animates the dot to the given scale. - */ - private void animateDotScale() { - float toScale = shouldDrawDot() ? 1f : 0f; - mDotIsAnimating = true; - - // Don't restart the animation if we're already animating to the given value. - if (mAnimatingToDotScale == toScale || !shouldDrawDot()) { - mDotIsAnimating = false; + void showDotIfNeeded(float fraction) { + if (!hasUnseenContent()) { return; } + setDotScale(fraction); + } + void showDotIfNeeded(boolean animate) { + // only show the dot if we have unseen content and it's not suppressed + if (!hasUnseenContent() || mDotSuppressedForBubbleUpdate) { + return; + } + if (animate) { + animateDotScale(1f); + } else { + setDotScale(1f); + } + } + + void hideDot() { + animateDotScale(0f); + } + + /** Marks this bubble such that it no longer has unseen content, and hides the dot. */ + void markSeen() { + if (mBubble instanceof BubbleBarBubble bubble) { + BubbleInfo info = bubble.getInfo(); + info.setFlags( + info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); + hideDot(); + } + } + + /** Animates the dot to the given scale. */ + private void animateDotScale(float toScale) { + boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0; + + // Don't restart the animation if we're already animating to the given value or if the dot + // scale is not changing + if ((mDotIsAnimating && mAnimatingToDotScale == toScale) || !isDotScaleChanging) { + return; + } + mDotIsAnimating = true; mAnimatingToDotScale = toScale; final boolean showDot = toScale > 0f; - // Do NOT wait until after animation ends to setShowDot - // to avoid overriding more recent showDot states. clearAnimation(); animate() - .setDuration(200) + .setDuration(SCALE_ANIMATION_DURATION_MS) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .setUpdateListener((valueAnimator) -> { float fraction = valueAnimator.getAnimatedFraction(); @@ -333,9 +472,42 @@ public class BubbleView extends ConstraintLayout { }).start(); } + /** + * Returns the distance from the top left corner of this bubble view to the center of its dot. + */ + public PointF getDotCenter() { + float[] dotPosition = + mOnLeft ? mDotRenderer.getLeftDotPosition() : mDotRenderer.getRightDotPosition(); + getDrawingRect(mTempBounds); + float dotCenterX = mTempBounds.width() * dotPosition[0]; + float dotCenterY = mTempBounds.height() * dotPosition[1]; + return new PointF(dotCenterX, dotCenterY); + } + + /** Returns the dot color. */ + public int getDotColor() { + return mDotColor; + } + @Override public String toString() { String toString = mBubble != null ? mBubble.getKey() : "null"; return "BubbleView{" + toString + "}"; } + + /** Interface for BubbleView to communicate with its controller */ + public interface Controller { + /** Get current bubble bar {@link BubbleBarLocation} */ + BubbleBarLocation getBubbleBarLocation(); + + /** This bubble should be dismissed */ + void dismiss(BubbleView bubble); + + /** Collapse the bubble bar */ + void collapse(); + + /** Request bubble bar location to be updated to the given location */ + void updateBubbleBarLocation(BubbleBarLocation location, + @BubbleBarLocation.UpdateSource int source); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/DragToBubbleController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/DragToBubbleController.kt new file mode 100644 index 0000000000..0c879b82c4 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/DragToBubbleController.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles + +import android.content.Context +import android.content.Intent +import android.view.WindowManager +import android.widget.FrameLayout +import com.android.launcher3.DropTarget.DragObject +import com.android.launcher3.dragndrop.DragController +import com.android.launcher3.dragndrop.DragOptions +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener +import com.android.launcher3.taskbar.bubbles.BubbleBarLocationDropTarget.BubbleBarDropTargetController +import com.android.quickstep.SystemUiProxy +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.ContextUtils.isRtl +import com.android.wm.shell.shared.bubbles.DeviceConfig +import com.android.wm.shell.shared.bubbles.DragToBubblesZoneChangeListener +import com.android.wm.shell.shared.bubbles.DragZone +import com.android.wm.shell.shared.bubbles.DragZoneFactory +import com.android.wm.shell.shared.bubbles.DragZoneFactory.BubbleBarPropertiesProvider +import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode +import com.android.wm.shell.shared.bubbles.DraggedObject +import com.android.wm.shell.shared.bubbles.DraggedObject.LauncherIcon +import com.android.wm.shell.shared.bubbles.DropTargetManager +import com.android.wm.shell.shared.bubbles.DropTargetManager.DragZoneChangedListener +import com.google.common.annotations.VisibleForTesting + +class DragToBubbleController(private val context: Context, bubbleBarContainer: FrameLayout) : + DragController.DragListener { + + @VisibleForTesting val dropTargetManager: DropTargetManager + @VisibleForTesting lateinit var bubbleBarLeftDropTarget: BubbleBarLocationDropTarget + @VisibleForTesting lateinit var bubbleBarRightDropTarget: BubbleBarLocationDropTarget + @VisibleForTesting lateinit var dragZoneFactory: DragZoneFactory + // If item drop is handled the next sysui update will set the bubble bar location + @VisibleForTesting var isItemDropHandled = false + private lateinit var bubbleBarLocationListener: BubbleBarLocationListener + private lateinit var systemUiProxy: SystemUiProxy + private lateinit var bubbleBarViewController: BubbleBarViewController + + init { + dropTargetManager = createDropTargetManager(bubbleBarContainer) + } + + fun init( + bubbleBarViewController: BubbleBarViewController, + bubbleBarPropertiesProvider: BubbleBarPropertiesProvider, + bubbleBarLocationListener: BubbleBarLocationListener, + systemUiProxy: SystemUiProxy, + ) { + this.bubbleBarViewController = bubbleBarViewController + this.systemUiProxy = systemUiProxy + this.bubbleBarLocationListener = bubbleBarLocationListener + val dropController: BubbleBarDropTargetController = createDropController() + dragZoneFactory = createDragZoneFactory(bubbleBarPropertiesProvider) + bubbleBarLeftDropTarget = createDropTarget(dropController, isLeftDropTarget = true) + bubbleBarRightDropTarget = createDropTarget(dropController, isLeftDropTarget = false) + } + + /** Adds bubble bar locations drop zones to the drag controller. */ + fun addBubbleBarDropTargets(dragController: DragController<*>) { + dragController.addDragListener(this) + dragController.addDropTarget(bubbleBarLeftDropTarget) + dragController.addDropTarget(bubbleBarRightDropTarget) + } + + /** Removes bubble bar locations drop zones to the drag controller. */ + fun removeBubbleBarDropTargets(dragController: DragController<*>) { + dragController.removeDragListener(this) + dragController.removeDropTarget(bubbleBarLeftDropTarget) + dragController.removeDropTarget(bubbleBarRightDropTarget) + } + + /** + * Runs the provided action once all drop target views are removed from the container. If there + * are no drop target views currently present or being animated, the action will be executed + * immediately. + */ + fun runAfterDropTargetsHidden(afterHiddenAction: Runnable) { + dropTargetManager.onDropTargetRemoved(afterHiddenAction) + } + + override fun onDragStart(dragObject: DragObject, options: DragOptions) { + isItemDropHandled = false + val launcherIcon: DraggedObject = LauncherIcon(bubbleBarViewController.hasBubbles()) {} + val dragZones: List = dragZoneFactory.createSortedDragZones(launcherIcon) + dropTargetManager.onDragStarted(launcherIcon, dragZones) + } + + override fun onDragEnd() { + dropTargetManager.onDragEnded() + } + + private fun createDropTargetManager(bubbleBarContainer: FrameLayout): DropTargetManager { + val listener: DragZoneChangedListener = + DragToBubblesZoneChangeListener( + context.isRtl, + object : DragToBubblesZoneChangeListener.Callback { + + private var currentBarLocation: BubbleBarLocation? = null + + override fun onDragEnteredLocation(bubbleBarLocation: BubbleBarLocation?) { + bubbleBarViewController.isShowingDropTarget = bubbleBarLocation != null + if (isItemDropHandled) return + val updatedLocation = bubbleBarLocation ?: getStartingBubbleBarLocation() + currentBarLocation = currentBarLocation ?: getStartingBubbleBarLocation() + if (updatedLocation != currentBarLocation) { + currentBarLocation = updatedLocation + bubbleBarLocationListener.onBubbleBarLocationAnimated(updatedLocation) + } + } + + override fun getStartingBubbleBarLocation(): BubbleBarLocation { + return bubbleBarViewController.bubbleBarLocation + ?: BubbleBarLocation.DEFAULT + } + + override fun hasBubbles(): Boolean = bubbleBarViewController.hasBubbles() + + override fun animateBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) { + if (isItemDropHandled) return + bubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation) + } + }, + ) + return DropTargetManager(context, bubbleBarContainer, listener) + } + + private fun createDragZoneFactory( + bubbleBarPropertiesProvider: BubbleBarPropertiesProvider + ): DragZoneFactory { + val splitScreenModeChecker = SplitScreenModeChecker { SplitScreenMode.NONE } + val desktopWindowModeChecker = DesktopWindowModeChecker { false } + val windowManager: WindowManager = context.getSystemService(WindowManager::class.java)!! + val deviceConfig: DeviceConfig = DeviceConfig.create(context, windowManager) + return DragZoneFactory( + context, + deviceConfig, + splitScreenModeChecker, + desktopWindowModeChecker, + bubbleBarPropertiesProvider, + ) + } + + private fun createDropController(): BubbleBarDropTargetController { + return object : BubbleBarDropTargetController { + override fun onDrop(itemInfo: ItemInfo, isLeftDropTarget: Boolean) { + isItemDropHandled = handleDrop(itemInfo, isLeftDropTarget) + } + + override fun acceptDrop(itemInfo: ItemInfo): Boolean { + return hasShortcutInfo(itemInfo) || itemInfo.intent?.component != null + } + + fun hasShortcutInfo(itemInfo: ItemInfo): Boolean { + return itemInfo is WorkspaceItemInfo && itemInfo.deepShortcutInfo != null + } + + private fun handleDrop(itemInfo: ItemInfo, isLeftDropTarget: Boolean): Boolean { + val location = + if (isLeftDropTarget) { + BubbleBarLocation.LEFT + } else { + BubbleBarLocation.RIGHT + } + if (hasShortcutInfo(itemInfo)) { + val si = (itemInfo as WorkspaceItemInfo).deepShortcutInfo + systemUiProxy.showShortcutBubble(si, location) + return true + } + val itemIntent: Intent = itemInfo.intent ?: return false + val packageName = itemIntent.component?.packageName ?: return false + itemIntent.setPackage(packageName) + systemUiProxy.showAppBubble(itemIntent, itemInfo.user, location) + return true + } + } + } + + private fun createDropTarget( + dropController: BubbleBarDropTargetController, + isLeftDropTarget: Boolean, + ) = + BubbleBarLocationDropTarget( + dropController, + dragZoneFactory, + dropTargetManager, + isLeftDropTarget, + ) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt new file mode 100644 index 0000000000..6fe0bdb1c5 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt @@ -0,0 +1,492 @@ +/* + * 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.launcher3.taskbar.bubbles.animation + +import android.animation.Animator +import android.animation.ValueAnimator +import kotlin.math.max +import kotlin.math.min + +/** + * Animates individual bubbles within the bubble bar while the bubble bar is expanded. + * + * This class should only be kept for the duration of the animation and a new instance should be + * created for each animation. + */ +class BubbleAnimator( + private val iconSize: Float, + private val expandedBarIconSpacing: Float, + private val bubbleCount: Int, + private val onLeft: Boolean, +) { + + companion object { + const val ANIMATION_DURATION_MS = 250L + } + + private var state: State = State.Idle + private lateinit var animator: ValueAnimator + + @JvmOverloads + fun animateNewBubble( + selectedBubbleIndex: Int, + newlySelectedBubbleIndex: Int? = null, + listener: Listener, + ) { + animator = createAnimator(listener) + state = State.AddingBubble(selectedBubbleIndex, newlySelectedBubbleIndex) + animator.start() + } + + fun animateRemovedBubble( + bubbleIndex: Int, + selectedBubbleIndex: Int, + removingLastBubble: Boolean, + removingLastRemainingBubble: Boolean, + listener: Listener, + ) { + animator = createAnimator(listener) + state = + State.RemovingBubble( + bubbleIndex = bubbleIndex, + selectedBubbleIndex = selectedBubbleIndex, + removingLastBubble = removingLastBubble, + removingLastRemainingBubble = removingLastRemainingBubble, + ) + animator.start() + } + + fun animateNewAndRemoveOld( + selectedBubbleIndex: Int, + newlySelectedBubbleIndex: Int, + removedBubbleIndex: Int, + addedBubbleIndex: Int, + listener: Listener, + ) { + animator = createAnimator(listener) + state = + State.AddingAndRemoving( + selectedBubbleIndex = selectedBubbleIndex, + newlySelectedBubbleIndex = newlySelectedBubbleIndex, + removedBubbleIndex = removedBubbleIndex, + addedBubbleIndex = addedBubbleIndex, + ) + animator.start() + } + + private fun createAnimator(listener: Listener): ValueAnimator { + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) + animator.addUpdateListener { animation -> + val animatedFraction = (animation as ValueAnimator).animatedFraction + listener.onAnimationUpdate(animatedFraction) + } + animator.addListener( + object : Animator.AnimatorListener { + + override fun onAnimationCancel(animation: Animator) { + listener.onAnimationCancel() + } + + override fun onAnimationEnd(animation: Animator) { + state = State.Idle + listener.onAnimationEnd() + } + + override fun onAnimationRepeat(animation: Animator) {} + + override fun onAnimationStart(animation: Animator) {} + } + ) + return animator + } + + /** + * The translation X of the bubble at index [bubbleIndex] when the bubble bar is expanded + * according to the progress of this animation. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getBubbleTranslationX(bubbleIndex: Int): Float { + return when (val state = state) { + State.Idle -> 0f + is State.AddingBubble -> + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = bubbleIndex, + scalingBubbleIndex = 0, + bubbleScale = animator.animatedFraction, + ) + + is State.RemovingBubble -> + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = bubbleIndex, + scalingBubbleIndex = state.bubbleIndex, + bubbleScale = 1 - animator.animatedFraction, + ) + + is State.AddingAndRemoving -> + getBubbleTranslationXWhileAddingBubbleAtLimit( + bubbleIndex = bubbleIndex, + removedBubbleIndex = state.removedBubbleIndex, + addedBubbleIndex = state.addedBubbleIndex, + addedBubbleScale = animator.animatedFraction, + removedBubbleScale = 1 - animator.animatedFraction, + ) + } + } + + /** + * The expanded width of the bubble bar according to the progress of the animation. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getExpandedWidth(): Float { + val bubbleScale = + when (state) { + State.Idle -> 0f + is State.AddingBubble -> animator.animatedFraction + is State.RemovingBubble -> 1 - animator.animatedFraction + is State.AddingAndRemoving -> { + // since we're adding a bubble and removing another bubble, their sizes together + // equal to a single bubble. the width is the same as having bubbleCount - 1 + // bubbles at full scale. + val totalSpace = (bubbleCount - 2) * expandedBarIconSpacing + val totalIconSize = (bubbleCount - 1) * iconSize + return totalIconSize + totalSpace + } + } + // When this animator is running the bubble bar is expanded so it's safe to assume that we + // have at least 2 bubbles, but should update the logic to support optional overflow. + // If we're removing the last bubble, the entire bar should animate and we shouldn't get + // here. + val totalSpace = (bubbleCount - 2 + bubbleScale) * expandedBarIconSpacing + val totalIconSize = (bubbleCount - 1 + bubbleScale) * iconSize + return totalIconSize + totalSpace + } + + /** + * Returns the arrow position according to the progress of the animation and, if the selected + * bubble is being removed, accounting to the newly selected bubble. + * + * Callers should verify that the animation is running before calling this. + * + * @see isRunning + */ + fun getArrowPosition(): Float { + return when (val state = state) { + State.Idle -> 0f + is State.AddingBubble -> getArrowPositionWhenAddingBubble(state) + is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state) + is State.AddingAndRemoving -> getArrowPositionWhenAddingAndRemovingBubble(state) + } + } + + private fun getArrowPositionWhenAddingBubble(state: State.AddingBubble): Float { + val scale = animator.animatedFraction + var tx = + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = state.selectedBubbleIndex, + scalingBubbleIndex = 0, + bubbleScale = scale, + ) + iconSize / 2f + if (state.newlySelectedBubbleIndex != null) { + val selectedBubbleScale = if (state.newlySelectedBubbleIndex == 0) scale else 1f + val finalTx = + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = state.newlySelectedBubbleIndex, + scalingBubbleIndex = 0, + bubbleScale = scale, + ) + iconSize * selectedBubbleScale / 2f + tx += (finalTx - tx) * animator.animatedFraction + } + return tx + } + + private fun getArrowPositionWhenRemovingBubble(state: State.RemovingBubble): Float = + if (state.selectedBubbleIndex != state.bubbleIndex || state.removingLastRemainingBubble) { + // if we're not removing the selected bubble or if we're removing the last remaining + // bubble, the selected bubble doesn't change so just return the translation X of the + // selected bubble and add half icon + val tx = + getBubbleTranslationXWhileScalingBubble( + bubbleIndex = state.selectedBubbleIndex, + scalingBubbleIndex = state.bubbleIndex, + bubbleScale = 1 - animator.animatedFraction, + ) + tx + iconSize / 2f + } else { + // we're removing the selected bubble so the arrow needs to point to a different bubble. + // if we're removing the last bubble the newly selected bubble will be the second to + // last. otherwise, it'll be the next bubble (closer to the overflow) + val iconAndSpacing = iconSize + expandedBarIconSpacing + if (state.removingLastBubble) { + if (onLeft) { + // the newly selected bubble is the bubble to the right. at the end of the + // animation all the bubbles will have shifted left, so the arrow stays at the + // same distance from the left edge of bar + (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f + } else { + // the newly selected bubble is the bubble to the left. at the end of the + // animation all the bubbles will have shifted right, and the arrow would + // eventually be closer to the left edge of the bar by iconAndSpacing + val initialTx = state.bubbleIndex * iconAndSpacing + iconSize / 2f + initialTx - animator.animatedFraction * iconAndSpacing + } + } else { + if (onLeft) { + // the newly selected bubble is to the left, and bubbles are shifting left, so + // move the arrow closer to the left edge of the bar by iconAndSpacing + val initialTx = + (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f + initialTx - animator.animatedFraction * iconAndSpacing + } else { + // the newly selected bubble is to the right, and bubbles are shifting right, so + // the arrow stays at the same distance from the left edge of the bar + state.bubbleIndex * iconAndSpacing + iconSize / 2f + } + } + } + + private fun getArrowPositionWhenAddingAndRemovingBubble(state: State.AddingAndRemoving): Float { + // The bubble bar keeps constant width while adding and removing bubble. So we just need to + // find selected bubble arrow position on the animation start and newly selected bubble + // arrow position on the animation end interpolating the arrow between these positions + // during the animation. + // The indexes in the state are provided for the bubble bar containing all bubbles. So for + // certain circumstances indexes should be adjusted. + // When animation is started added bubble has zero scale as well as removed bubble when the + // animation is ended, so for both cases we should compute translation as it is one less + // bubble. + val bubbleCountOnEnd = bubbleCount - 1 + var selectedIndex = state.selectedBubbleIndex + // We only need to adjust the selected index if added bubble was added before the selected. + if (selectedIndex > state.addedBubbleIndex) { + // If the selectedIndex is higher index than the added bubble index, we need to reduce + // selectedIndex by one because the added bubble has zero scale when animation is + // started. + selectedIndex-- + } + var newlySelectedIndex = state.newlySelectedBubbleIndex + // We only need to adjust newlySelectedIndex if removed bubble was removed before the newly + // selected bubble. + if (newlySelectedIndex > state.removedBubbleIndex) { + // If the newlySelectedIndex is higher index than the removed bubble index, we need to + // reduce newlySelectedIndex by one because the removed bubble has zero scale when + // animation is ended. + newlySelectedIndex-- + } + val iconAndSpacing: Float = iconSize + expandedBarIconSpacing + val startTx = getBubblesToTheLeft(selectedIndex, bubbleCountOnEnd) * iconAndSpacing + val endTx = getBubblesToTheLeft(newlySelectedIndex, bubbleCountOnEnd) * iconAndSpacing + val tx = startTx + (endTx - startTx) * animator.animatedFraction + return tx + iconSize / 2f + } + + private fun getBubblesToTheLeft(bubbleIndex: Int, bubbleCount: Int = this.bubbleCount): Int = + // when bar is on left the index - 0 corresponds to the right - most bubble and when the + // bubble bar is on the right - 0 corresponds to the left - most bubble. + if (onLeft) bubbleCount - bubbleIndex - 1 else bubbleIndex + + /** + * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is + * expanded and a bubble is animating in or out. + * + * @param bubbleIndex the index of the bubble for which the translation is requested + * @param scalingBubbleIndex the index of the bubble that is animating + * @param bubbleScale the current scale of the animating bubble + */ + private fun getBubbleTranslationXWhileScalingBubble( + bubbleIndex: Int, + scalingBubbleIndex: Int, + bubbleScale: Float, + ): Float { + val iconAndSpacing = iconSize + expandedBarIconSpacing + // the bubble is scaling from the center, so we need to adjust its translation so + // that the distance to the adjacent bubble scales at the same rate. + val pivotAdjustment = -(1 - bubbleScale) * iconSize / 2f + + return if (onLeft) { + when { + bubbleIndex < scalingBubbleIndex -> + // the bar is on the left and the current bubble is to the right of the scaling + // bubble so account for its scale + (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing + + bubbleIndex == scalingBubbleIndex -> { + // the bar is on the left and this is the scaling bubble + val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize + // don't count the spacing between the scaling bubble and the bubble on the left + // because we need to scale that space + val totalSpacing = (bubbleCount - bubbleIndex - 2) * expandedBarIconSpacing + val scaledSpace = bubbleScale * expandedBarIconSpacing + totalIconSize + totalSpacing + scaledSpace + pivotAdjustment + } + + else -> + // the bar is on the left and the scaling bubble is on the right. the current + // bubble is unaffected by the scaling bubble + (bubbleCount - bubbleIndex - 1) * iconAndSpacing + } + } else { + when { + bubbleIndex < scalingBubbleIndex -> + // the bar is on the right and the scaling bubble is on the right. the current + // bubble is unaffected by the scaling bubble + iconAndSpacing * bubbleIndex + + bubbleIndex == scalingBubbleIndex -> + // the bar is on the right, and this is the animating bubble. it only needs to + // be adjusted for the scaling pivot. + iconAndSpacing * bubbleIndex + pivotAdjustment + + else -> + // the bar is on the right and the scaling bubble is on the left so account for + // its scale + iconAndSpacing * (bubbleIndex - 1 + bubbleScale) + } + } + } + + private fun getBubbleTranslationXWhileAddingBubbleAtLimit( + bubbleIndex: Int, + removedBubbleIndex: Int, + addedBubbleIndex: Int, + addedBubbleScale: Float, + removedBubbleScale: Float, + ): Float { + val iconAndSpacing = iconSize + expandedBarIconSpacing + // the bubbles are scaling from the center, so we need to adjust their translation so + // that the distance to the adjacent bubble scales at the same rate. + val addedBubblePivotAdjustment = (addedBubbleScale - 1) * iconSize / 2f + val removedBubblePivotAdjustment = (removedBubbleScale - 1) * iconSize / 2f + + val minAddedRemovedIndex = min(addedBubbleIndex, removedBubbleIndex) + val maxAddedRemovedIndex = max(addedBubbleIndex, removedBubbleIndex) + val isBetweenAddedAndRemoved = + bubbleIndex in (minAddedRemovedIndex + 1).. { + if (isRemovedBubbleToLeftOfAddedBubble) { + // the removed bubble is to the left so account for it + (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing + } else { + // the added bubble is to the left so account for it + (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing + } + } + + bubbleIndex == addedBubbleIndex -> { + if (isRemovedBubbleToLeftOfAddedBubble) { + // the removed bubble is to the left so account for it + (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing + } else { + // it's the left-most scaling bubble, all bubbles on the left are at full scale + bubblesToLeft * iconAndSpacing + } + addedBubblePivotAdjustment + } + + bubbleIndex == removedBubbleIndex -> { + if (isRemovedBubbleToLeftOfAddedBubble) { + // All the bubbles to the left are at full scale, but we need to scale the + // spacing between the removed bubble and the bubble next to it + val totalIconSize = bubblesToLeft * iconSize + val totalSpacing = + (bubblesToLeft - 1 + removedBubbleScale) * expandedBarIconSpacing + totalIconSize + totalSpacing + } else { + // The added bubble is to the left, so account for it + (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing + } + removedBubblePivotAdjustment + } + + else -> { + // if bubble index is on the right side of the animated bubbles, we need to deduct + // one, since both the added and the removed bubbles takes a single place + val onTheRightOfAnimatedBubbles = + if (onLeft) { + bubbleIndex < minAddedRemovedIndex + } else { + bubbleIndex > maxAddedRemovedIndex + } + (bubblesToLeft - if (onTheRightOfAnimatedBubbles) 1 else 0) * iconAndSpacing + } + } + } + + val isRunning: Boolean + get() = state != State.Idle + + /** The state of the animation. */ + sealed interface State { + + /** The animation is not running. */ + data object Idle : State + + /** A new bubble is being added to the bubble bar. */ + data class AddingBubble( + /** The index of the selected bubble. */ + val selectedBubbleIndex: Int, + /** The index of the newly selected bubble. */ + val newlySelectedBubbleIndex: Int?, + ) : State + + /** A bubble is being removed from the bubble bar. */ + data class RemovingBubble( + /** The index of the bubble being removed. */ + val bubbleIndex: Int, + /** The index of the selected bubble. */ + val selectedBubbleIndex: Int, + /** Whether the bubble being removed is also the last bubble. */ + val removingLastBubble: Boolean, + /** Whether we're removing the last remaining bubble. */ + val removingLastRemainingBubble: Boolean, + ) : State + + /** A new bubble is being added and an old bubble is being removed from the bubble bar. */ + data class AddingAndRemoving( + /** The index of the selected bubble. */ + val selectedBubbleIndex: Int, + /** The index of the newly selected bubble. */ + val newlySelectedBubbleIndex: Int, + /** The index of the bubble being removed. */ + val removedBubbleIndex: Int, + /** The index of the added bubble. */ + val addedBubbleIndex: Int, + ) : State + } + + /** Callbacks for the animation. */ + interface Listener { + + /** + * Notifies the listener of an animation update event, where `animatedFraction` represents + * the progress of the animation starting from 0 and ending at 1. + */ + fun onAnimationUpdate(animatedFraction: Float) + + /** Notifies the listener that the animation was canceled. */ + fun onAnimationCancel() + + /** Notifies that listener that the animation ended. */ + fun onAnimationEnd() + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt index d88e272332..15fe35b3cc 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -16,14 +16,21 @@ package com.android.launcher3.taskbar.bubbles.animation +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator import android.view.View import android.view.View.VISIBLE import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.R import com.android.launcher3.taskbar.bubbles.BubbleBarBubble +import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier import com.android.launcher3.taskbar.bubbles.BubbleBarView -import com.android.launcher3.taskbar.bubbles.BubbleStashController import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController import com.android.wm.shell.shared.animation.PhysicsAnimator /** Handles animations for bubble bar bubbles. */ @@ -32,26 +39,69 @@ class BubbleBarViewAnimator constructor( private val bubbleBarView: BubbleBarView, private val bubbleStashController: BubbleStashController, - private val scheduler: Scheduler = HandlerScheduler(bubbleBarView) + private val bubbleBarFlyoutController: BubbleBarFlyoutController, + private val bubbleBarParentViewHeightUpdateNotifier: BubbleBarParentViewHeightUpdateNotifier, + private val onExpanded: Runnable, + private val onBubbleBarVisible: Runnable, + private val scheduler: Scheduler = HandlerScheduler(bubbleBarView), ) { private var animatingBubble: AnimatingBubble? = null + private val bubbleBarBounceDistanceInPx = + bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance) + + fun hasAnimation() = animatingBubble != null + + val isAnimating: Boolean + get() { + val animatingBubble = animatingBubble ?: return false + return animatingBubble.state != AnimatingBubble.State.CREATED + } + + private var interceptedHandleAnimator = false private companion object { /** The time to show the flyout. */ - const val FLYOUT_DELAY_MS: Long = 2500 + const val FLYOUT_DELAY_MS: Long = 3000 /** The initial scale Y value that the new bubble is set to before the animation starts. */ const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f /** The minimum alpha value to make the bubble bar touchable. */ const val MIN_ALPHA_FOR_TOUCHABLE = 0.5f + /** The duration of the bounce animation. */ + const val BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS = 250L } /** Wrapper around the animating bubble with its show and hide animations. */ private data class AnimatingBubble( val bubbleView: BubbleView, val showAnimation: Runnable, - val hideAnimation: Runnable - ) + val hideAnimation: Runnable, + val expand: Boolean, + val state: State = State.CREATED, + ) { + + /** + * The state of the animation. + * + * The animation is initially created but will be scheduled later using the [Scheduler]. + * + * The normal uninterrupted cycle is for the bubble notification to animate in, then be in a + * transient state and eventually to animate out. + * + * However different events, such as touch and external signals, may cause the animation to + * end earlier. + */ + enum class State { + /** The animation is created but not started yet. */ + CREATED, + /** The bubble notification is animating in. */ + ANIMATING_IN, + /** The bubble notification is now fully showing and waiting to be hidden. */ + IN, + /** The bubble notification is animating out. */ + ANIMATING_OUT, + } + } /** An interface for scheduling jobs. */ interface Scheduler { @@ -85,19 +135,34 @@ constructor( private val springConfig = PhysicsAnimator.SpringConfig( stiffness = SpringForce.STIFFNESS_LOW, - dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY + dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, ) + private fun cancelAnimationIfPending() { + val animatingBubble = animatingBubble ?: return + if (animatingBubble.state != AnimatingBubble.State.CREATED) return + scheduler.cancel(animatingBubble.showAnimation) + scheduler.cancel(animatingBubble.hideAnimation) + } + /** Animates a bubble for the state where the bubble bar is stashed. */ - fun animateBubbleInForStashed(b: BubbleBarBubble) { + fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) { + if (isAnimating) { + interruptAndUpdateAnimatingBubble(b.view, isExpanding) + return + } + cancelAnimationIfPending() + val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() - // the animation of a new bubble is divided into 2 parts. The first part shows the bubble - // and the second part hides it after a delay. + // the animation of a new bubble is divided into 2 parts. The first part transforms the + // handle to the bubble bar and then shows the flyout. The second part hides the flyout and + // transforms the bubble bar back to the handle. val showAnimation = buildHandleToBubbleBarAnimation() - val hideAnimation = buildBubbleBarToHandleAnimation() - animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation() + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } @@ -116,40 +181,48 @@ constructor( * 3. The third part is the overshoot of the spring animation, where we make the bubble fully * visible which helps avoiding further updates when we re-enter the second part. */ - private fun buildHandleToBubbleBarAnimation() = Runnable { - // prepare the bubble bar for the animation - bubbleBarView.onAnimatingBubbleStarted() - bubbleBarView.visibility = VISIBLE - bubbleBarView.alpha = 0f - bubbleBarView.translationY = 0f - bubbleBarView.scaleX = 1f - bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y - bubbleBarView.relativePivotY = 0.5f + private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) + // prepare the bubble bar for the animation if we're starting fresh + if (initialVelocity == null) { + bubbleBarView.visibility = VISIBLE + bubbleBarView.alpha = 0f + bubbleBarView.translationY = 0f + bubbleBarView.scaleX = 1f + bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y + bubbleBarView.setBackgroundScaleX(1f) + bubbleBarView.setBackgroundScaleY(1f) + bubbleBarView.relativePivotY = 0.5f + } // this is the offset between the center of the bubble bar and the center of the stash // handle. when the handle becomes invisible and we start animating in the bubble bar, // the translation y is offset by this value to make the transition from the handle to the // bar smooth. - val offset = bubbleStashController.diffBetweenHandleAndBarCenters + val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() + val stashedHandleTranslationYForAnimation = + bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() val stashedHandleTranslationY = - bubbleStashController.stashedHandleTranslationForNewBubbleAnimation + bubbleStashController.getHandleTranslationY() ?: return@Runnable + val translationTracker = TranslationTracker(stashedHandleTranslationY) // this is the total distance that both the stashed handle and the bubble will be traveling // at the end of the animation the bubble bar will be positioned in the same place when it // shows while we're in an app. val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset - val animator = bubbleStashController.stashedHandlePhysicsAnimator + val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable animator.setDefaultSpringConfig(springConfig) - animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY) + animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f) animator.addUpdateListener { handle, values -> val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener + if (animatingBubble == null) return@addUpdateListener when { - ty >= stashedHandleTranslationY -> { + ty >= stashedHandleTranslationYForAnimation -> { // we're in the first leg of the animation. only animate the handle. the bubble // bar remains hidden during this part of the animation // map the path [0, stashedHandleTranslationY] to [0,1] - val fraction = ty / stashedHandleTranslationY + val fraction = ty / stashedHandleTranslationYForAnimation handle.alpha = 1 - fraction } ty >= totalTranslationY -> { @@ -163,8 +236,8 @@ constructor( if (bubbleBarView.alpha != 1f) { // map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1] val fraction = - (ty - stashedHandleTranslationY) / - (totalTranslationY - stashedHandleTranslationY) + (ty - stashedHandleTranslationYForAnimation) / + (totalTranslationY - stashedHandleTranslationYForAnimation) bubbleBarView.alpha = fraction bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y + @@ -184,18 +257,17 @@ constructor( bubbleStashController.updateTaskbarTouchRegion() } } + translationTracker.updateTyAndExpandIfNeeded(ty) } animator.addEndListener { _, _, _, canceled, _, _, _ -> // if the show animation was canceled, also cancel the hide animation. this is typically // canceled in this class, but could potentially be canceled elsewhere. - if (canceled) { - val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener - scheduler.cancel(hideAnimation) - animatingBubble = null - bubbleBarView.onAnimatingBubbleCompleted() - bubbleBarView.relativePivotY = 1f + if (canceled || animatingBubble?.expand == true) { + cancelHideAnimation() return@addEndListener } + setupAndShowFlyout() + // the bubble bar is now fully settled in. update taskbar touch region so it's touchable bubbleStashController.updateTaskbarTouchRegion() } @@ -218,13 +290,14 @@ constructor( */ private fun buildBubbleBarToHandleAnimation() = Runnable { if (animatingBubble == null) return@Runnable - val offset = bubbleStashController.diffBetweenHandleAndBarCenters + moveToState(AnimatingBubble.State.ANIMATING_OUT) + val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters() val stashedHandleTranslationY = - bubbleStashController.stashedHandleTranslationForNewBubbleAnimation + bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation() // this is the total distance that both the stashed handle and the bar will be traveling val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset bubbleStashController.setHandleTranslationY(totalTranslationY) - val animator = bubbleStashController.stashedHandlePhysicsAnimator + val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, 0f) animator.addUpdateListener { handle, values -> @@ -260,88 +333,430 @@ constructor( } } } - animator.addEndListener { _, _, _, canceled, _, _, _ -> - animatingBubble = null + animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ -> + // PhysicsAnimator calls the end listeners when the animation is replaced with a new one + // if we're not in ANIMATING_OUT state, then this animation never started and we should + // return + if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener + if (interceptedHandleAnimator) { + interceptedHandleAnimator = false + // post this to give a PhysicsAnimator a chance to clean up its internal listeners. + // otherwise this end listener will be called as soon as we create a new spring + // animation + scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity)) + return@addEndListener + } + clearAnimatingBubble() if (!canceled) bubbleStashController.stashBubbleBarImmediate() - bubbleBarView.onAnimatingBubbleCompleted() bubbleBarView.relativePivotY = 1f + bubbleBarView.scaleY = 1f bubbleStashController.updateTaskbarTouchRegion() } - animator.start() + + val bubble = animatingBubble?.bubbleView?.bubble as? BubbleBarBubble + val flyout = bubble?.flyoutMessage + if (flyout != null) { + bubbleBarFlyoutController.collapseFlyout { + onFlyoutRemoved() + animator.start() + } + } else { + animator.start() + } } /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */ - fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) { + fun animateToInitialState( + b: BubbleBarBubble, + isInApp: Boolean, + isExpanding: Boolean, + isDragging: Boolean = false, + ) { val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() - // the animation of a new bubble is divided into 2 parts. The first part shows the bubble - // and the second part hides it after a delay if we are in an app. - val showAnimation = buildBubbleBarBounceAnimation() + // the animation of a new bubble is divided into 2 parts. The first part slides in the + // bubble bar and shows the flyout. The second part hides the flyout and transforms the + // bubble bar to the handle if we're in an app. + val showAnimation = buildBubbleBarSpringInAnimation() val hideAnimation = - if (isInApp && !isExpanding) { + if (isInApp && !isExpanding && !isDragging) { buildBubbleBarToHandleAnimation() } else { - // in this case the bubble bar remains visible so not much to do. once we implement - // the flyout we'll update this runnable to hide it. Runnable { - animatingBubble = null + collapseFlyoutAndUpdateState() + if (isDragging) return@Runnable bubbleStashController.showBubbleBarImmediate() - bubbleBarView.onAnimatingBubbleCompleted() bubbleStashController.updateTaskbarTouchRegion() } } - animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation) + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } - private fun buildBubbleBarBounceAnimation() = Runnable { + private fun buildBubbleBarSpringInAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) // prepare the bubble bar for the animation - bubbleBarView.onAnimatingBubbleStarted() bubbleBarView.translationY = bubbleBarView.height.toFloat() bubbleBarView.visibility = VISIBLE + onBubbleBarVisible.run() bubbleBarView.alpha = 1f bubbleBarView.scaleX = 1f bubbleBarView.scaleY = 1f + bubbleBarView.setBackgroundScaleX(1f) + bubbleBarView.setBackgroundScaleY(1f) + + val translationTracker = TranslationTracker(bubbleBarView.translationY) val animator = PhysicsAnimator.getInstance(bubbleBarView) animator.setDefaultSpringConfig(springConfig) animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY) - animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() } + animator.addUpdateListener { _, values -> + val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener + translationTracker.updateTyAndExpandIfNeeded(ty) + bubbleStashController.updateTaskbarTouchRegion() + } animator.addEndListener { _, _, _, _, _, _, _ -> + if (animatingBubble?.expand == true) { + cancelHideAnimation() + } else { + setupAndShowFlyout() + } // the bubble bar is now fully settled in. update taskbar touch region so it's touchable bubbleStashController.updateTaskbarTouchRegion() } animator.start() } - /** Handles touching the animating bubble bar. */ - fun onBubbleBarTouchedWhileAnimating() { + fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) { + if (isAnimating) { + interruptAndUpdateAnimatingBubble(b.view, isExpanding) + return + } + cancelAnimationIfPending() + + val bubbleView = b.view + val animator = PhysicsAnimator.getInstance(bubbleView) + if (animator.isRunning()) animator.cancel() + // first bounce the bubble bar and show the flyout. Then hide the flyout. + val showAnimation = buildBubbleBarBounceAnimation() + val hideAnimation = Runnable { + collapseFlyoutAndUpdateState() + bubbleStashController.showBubbleBarImmediate() + bubbleStashController.updateTaskbarTouchRegion() + } + animatingBubble = + AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding) + scheduler.post(showAnimation) + scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) + } + + private fun collapseFlyoutAndUpdateState() { + moveToState(AnimatingBubble.State.ANIMATING_OUT) + bubbleBarFlyoutController.collapseFlyout { + onFlyoutRemoved() + clearAnimatingBubble() + } + } + + /** + * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first + * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends + * the bubble bar moves back to its initial position with a spring animation. + */ + private fun buildBubbleBarBounceAnimation() = Runnable { + moveToState(AnimatingBubble.State.ANIMATING_IN) + val ty = bubbleStashController.bubbleBarTranslationY + + val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView) + springBackAnimation.setDefaultSpringConfig(springConfig) + springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty) + springBackAnimation.addEndListener { _, _, _, _, _, _, _ -> + if (animatingBubble?.expand == true) { + expandBubbleBar() + cancelHideAnimation() + } else { + setupAndShowFlyout() + } + } + + // animate the bubble bar up and start the spring back down animation when it ends. + ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx) + .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS) + .withEndAction { + springBackAnimation.start() + if (animatingBubble?.expand == true) expandBubbleBar() + } + .start() + } + + private fun setupAndShowFlyout() { + val bubbleView = animatingBubble?.bubbleView + val bubble = bubbleView?.bubble as? BubbleBarBubble + val flyout = bubble?.flyoutMessage + if (flyout != null) { + bubbleBarFlyoutController.setUpAndShowFlyout( + BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message), + onInit = { bubbleView.suppressDotForBubbleUpdate() }, + onEnd = { + moveToState(AnimatingBubble.State.IN) + bubbleStashController.updateTaskbarTouchRegion() + }, + ) + } else { + moveToState(AnimatingBubble.State.IN) + } + } + + private fun cancelFlyout() { + animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ true) + bubbleBarFlyoutController.cancelFlyout { bubbleStashController.updateTaskbarTouchRegion() } + } + + private fun onFlyoutRemoved() { + animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ false) + bubbleStashController.updateTaskbarTouchRegion() + } + + /** Interrupts the animation due to touching the bubble bar or flyout. */ + fun interruptForTouch() { + animatingBubble?.hideAnimation?.let { scheduler.cancel(it) } PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning() - bubbleStashController.stashedHandlePhysicsAnimator.cancelIfRunning() - val hideAnimation = animatingBubble?.hideAnimation ?: return - scheduler.cancel(hideAnimation) - bubbleBarView.onAnimatingBubbleCompleted() - bubbleBarView.relativePivotY = 1f - animatingBubble = null + bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() + cancelFlyout() + resetBubbleBarPropertiesOnInterrupt() + clearAnimatingBubble() } /** Notifies the animator that the taskbar area was touched during an animation. */ fun onStashStateChangingWhileAnimating() { - val hideAnimation = animatingBubble?.hideAnimation ?: return - scheduler.cancel(hideAnimation) - animatingBubble = null - bubbleStashController.stashedHandlePhysicsAnimator.cancel() - bubbleBarView.onAnimatingBubbleCompleted() - bubbleBarView.relativePivotY = 1f + animatingBubble?.hideAnimation?.let { scheduler.cancel(it) } + cancelFlyout() + clearAnimatingBubble() + bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() + resetBubbleBarPropertiesOnInterrupt() bubbleStashController.onNewBubbleAnimationInterrupted( - /* isStashed= */ bubbleBarView.alpha == 0f, - bubbleBarView.translationY + /* isStashed= */ bubbleStashController.isStashed, + bubbleBarView.translationY, ) } - private fun PhysicsAnimator.cancelIfRunning() { - if (isRunning()) cancel() + /** Interrupts the animation due to the IME becoming visible. */ + fun interruptForIme() { + cancelFlyout() + val hideAnimation = animatingBubble?.hideAnimation ?: return + scheduler.cancel(hideAnimation) + animatingBubble = null + bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning() + resetBubbleBarPropertiesOnInterrupt() + // stash the bubble bar since the IME is now visible + bubbleStashController.onNewBubbleAnimationInterrupted( + /* isStashed= */ true, + bubbleBarView.translationY, + ) + } + + fun expandedWhileAnimating() { + val animatingBubble = animatingBubble ?: return + this.animatingBubble = animatingBubble.copy(expand = true) + // if we're fully in and waiting to hide, cancel the hide animation and clean up + if (animatingBubble.state == AnimatingBubble.State.IN) { + cancelFlyout() + expandBubbleBar() + cancelHideAnimation() + } + } + + private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) { + val animatingBubble = animatingBubble ?: return + when (animatingBubble.state) { + AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started + AnimatingBubble.State.ANIMATING_IN -> + updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding) + AnimatingBubble.State.IN -> + updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding) + AnimatingBubble.State.ANIMATING_OUT -> + updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding) + } + } + + private fun updateAnimationWhileAnimatingIn( + animatingBubble: AnimatingBubble, + bubbleView: BubbleView, + isExpanding: Boolean, + ) { + this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) + if (!bubbleBarFlyoutController.hasFlyout()) { + // if the flyout does not yet exist, then we're only animating the bubble bar. + // the animating bubble has been updated, so the when the flyout expands it will + // show the right message. we only need to update the dot visibility. + bubbleView.updateDotVisibility(/* animate= */ !bubbleStashController.isStashed) + return + } + + val bubble = bubbleView.bubble as? BubbleBarBubble + val flyout = bubble?.flyoutMessage + if (flyout != null) { + // the flyout is currently expanding and we need to update it with new data + bubbleView.suppressDotForBubbleUpdate() + bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout) + } else { + // the flyout is expanding but we don't have new flyout data to update it with, + // so cancel the expanding flyout. + cancelFlyout() + } + } + + private fun updateAnimationWhileIn( + animatingBubble: AnimatingBubble, + bubbleView: BubbleView, + isExpanding: Boolean, + ) { + // unsuppress the current bubble because we are about to hide its flyout + animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false) + this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) + + // we're currently idle, waiting for the hide animation to start. update the flyout + // data and reschedule the hide animation to run later to give the user a chance to + // see the new flyout. + val hideAnimation = animatingBubble.hideAnimation + scheduler.cancel(hideAnimation) + scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) + + val bubble = bubbleView.bubble as? BubbleBarBubble + val flyout = bubble?.flyoutMessage + if (flyout != null) { + bubbleView.suppressDotForBubbleUpdate() + bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) { + bubbleStashController.updateTaskbarTouchRegion() + } + } else { + cancelFlyout() + } + } + + private fun updateAnimationWhileAnimatingOut( + animatingBubble: AnimatingBubble, + bubbleView: BubbleView, + isExpanding: Boolean, + ) { + // unsuppress the current bubble because we are about to hide its flyout + animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false) + this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding) + + // the hide animation already started so it can't be canceled, just post it again + val hideAnimation = animatingBubble.hideAnimation + scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) + + val bubble = bubbleView.bubble as? BubbleBarBubble + val flyout = bubble?.flyoutMessage + if (bubbleBarFlyoutController.hasFlyout()) { + // the flyout is collapsing. update it with the new flyout + if (flyout != null) { + moveToState(AnimatingBubble.State.ANIMATING_IN) + bubbleView.suppressDotForBubbleUpdate() + bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) { + moveToState(AnimatingBubble.State.IN) + bubbleStashController.updateTaskbarTouchRegion() + } + } else { + cancelFlyout() + moveToState(AnimatingBubble.State.IN) + } + } else { + // the flyout is already gone. if we're animating the handle cancel it. the + // animation itself can handle morphing back into the bubble bar and restarting + // and show the flyout. + val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator() + if (handleAnimator != null && handleAnimator.isRunning()) { + interceptedHandleAnimator = true + handleAnimator.cancel() + } + + // if we're not animating the handle, then the hide animation simply hides the + // flyout, but if the flyout is gone then the animation has ended. + } + } + + private fun cancelHideAnimation() { + val hideAnimation = animatingBubble?.hideAnimation ?: return + scheduler.cancel(hideAnimation) + clearAnimatingBubble() + bubbleBarView.relativePivotY = 1f + bubbleStashController.showBubbleBarImmediate() + } + + private fun resetBubbleBarPropertiesOnInterrupt() { + bubbleBarView.relativePivotY = 1f + bubbleBarView.scaleX = 1f + bubbleBarView.scaleY = 1f + } + + private fun PhysicsAnimator?.cancelIfRunning() { + if (this?.isRunning() == true) cancel() + } + + private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator { + setDuration(duration) + return this + } + + private fun ObjectAnimator.withEndAction(endAction: () -> Unit): ObjectAnimator { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + endAction() + } + } + ) + return this + } + + private fun moveToState(state: AnimatingBubble.State) { + val animatingBubble = this.animatingBubble ?: return + this.animatingBubble = animatingBubble.copy(state = state) + if (state == AnimatingBubble.State.ANIMATING_IN) { + bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary() + } + } + + private fun clearAnimatingBubble() { + animatingBubble = null + bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary() + } + + private fun expandBubbleBar() { + bubbleBarView.animateExpanded(true) + onExpanded.run() + } + + /** + * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands + * as part of the animation, the expansion should start after the bubble bar reaches the peak + * position. + */ + private inner class TranslationTracker(initialTy: Float) { + private var previousTy = initialTy + private var startedExpanding = false + private var reachedPeak = false + + fun updateTyAndExpandIfNeeded(ty: Float) { + if (!reachedPeak) { + // the bubble bar is positioned at the bottom of the screen and moves up using + // negative ty values. the peak is reached the first time we see a value that is + // greater than the previous. + if (ty > previousTy) { + reachedPeak = true + } + } + val expand = animatingBubble?.expand ?: false + if (reachedPeak && expand && !startedExpanding) { + expandBubbleBar() + startedExpanding = true + } + previousTy = ty + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt new file mode 100644 index 0000000000..4a2f029fd5 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt @@ -0,0 +1,188 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.animation.ValueAnimator +import android.graphics.Rect +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.animation.addListener +import com.android.app.animation.Interpolators +import com.android.launcher3.R + +/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */ +class BubbleBarFlyoutController +@JvmOverloads +constructor( + private val container: FrameLayout, + private val positioner: BubbleBarFlyoutPositioner, + private val callbacks: FlyoutCallbacks, + private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container), +) { + + val maximumFlyoutHeight: Int = BubbleBarFlyoutView.getMaximumViewHeight(container.context) + + private companion object { + const val EXPAND_ANIMATION_DURATION_MS = 400L + const val COLLAPSE_ANIMATION_DURATION_MS = 350L + } + + private var flyout: BubbleBarFlyoutView? = null + private var animator: ValueAnimator? = null + private val horizontalMargin = + container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin) + + private enum class AnimationType { + /** Morphs the flyout between a dot and a rounded rectangle. */ + MORPH, + /** Fades the flyout in or out. */ + FADE, + } + + /** The bounds of the flyout. */ + val flyoutBounds: Rect? + get() { + val flyout = this.flyout ?: return null + val rect = Rect(flyout.bounds) + rect.offset(0, flyout.translationY.toInt()) + return rect + } + + fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onInit: () -> Unit, onEnd: () -> Unit) { + animator?.cancel() + flyout?.let(container::removeView) + val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler) + + flyout.translationY = positioner.targetTy + + val lp = + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM or if (positioner.isOnLeft) Gravity.LEFT else Gravity.RIGHT, + ) + lp.marginStart = horizontalMargin + lp.marginEnd = horizontalMargin + container.addView(flyout, lp) + + this.flyout = flyout + flyout.showFromCollapsed(message) { + flyout.updateExpansionProgress(0f) + onInit() + showFlyout(AnimationType.MORPH, onEnd) + } + } + + private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) { + val flyout = this.flyout ?: return + val startValue = getCurrentAnimatedValueIfRunning() ?: 0f + val duration = (EXPAND_ANIMATION_DURATION_MS * (1f - startValue)).toLong() + animator?.cancel() + val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration) + animator.interpolator = Interpolators.EMPHASIZED + this.animator = animator + when (animationType) { + AnimationType.FADE -> + animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float } + AnimationType.MORPH -> + animator.addUpdateListener { _ -> + flyout.updateExpansionProgress(animator.animatedValue as Float) + } + } + animator.addListener( + onEnd = { + endAction() + flyout.setOnClickListener { callbacks.flyoutClicked() } + } + ) + animator.start() + } + + fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) { + val flyout = flyout ?: return + hideFlyout(AnimationType.FADE) { + flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) } + } + } + + fun updateFlyoutWhileExpanding(message: BubbleBarFlyoutMessage) { + val flyout = flyout ?: return + flyout.updateData(message) {} + } + + fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) { + val flyout = flyout ?: return + animator?.pause() + animator?.removeAllListeners() + flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) } + } + + fun cancelFlyout(endAction: () -> Unit) { + hideFlyout(AnimationType.FADE) { + cleanupFlyoutView() + endAction() + } + } + + fun collapseFlyout(endAction: () -> Unit) { + hideFlyout(AnimationType.MORPH) { + cleanupFlyoutView() + endAction() + } + } + + private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) { + val flyout = this.flyout ?: return + val startValue = getCurrentAnimatedValueIfRunning() ?: 1f + val duration = (COLLAPSE_ANIMATION_DURATION_MS * startValue).toLong() + animator?.cancel() + val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration) + animator.interpolator = Interpolators.EMPHASIZED + this.animator = animator + when (animationType) { + AnimationType.FADE -> + animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float } + AnimationType.MORPH -> + animator.addUpdateListener { _ -> + flyout.updateExpansionProgress(animator.animatedValue as Float) + } + } + animator.addListener( + onStart = { + flyout.setOnClickListener(null) + if (animationType == AnimationType.MORPH) { + flyout.updateTranslationToCollapsedPosition() + } + }, + onEnd = { endAction() }, + ) + animator.start() + } + + private fun cleanupFlyoutView() { + container.removeView(flyout) + this@BubbleBarFlyoutController.flyout = null + } + + fun hasFlyout() = flyout != null + + private fun getCurrentAnimatedValueIfRunning(): Float? { + val animator = animator ?: return null + return if (animator.isRunning) animator.animatedValue as Float else null + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt new file mode 100644 index 0000000000..14b456c73f --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt @@ -0,0 +1,21 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.graphics.drawable.Drawable + +data class BubbleBarFlyoutMessage(val icon: Drawable?, val title: String, val message: String) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt new file mode 100644 index 0000000000..aa2555e865 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt @@ -0,0 +1,51 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.graphics.PointF + +/** Provides positioning data to the flyout view. */ +interface BubbleBarFlyoutPositioner { + + /** Whether the flyout view should be positioned on left or the right edge. */ + val isOnLeft: Boolean + + /** The target translation Y that the flyout view should have when displayed. */ + val targetTy: Float + + /** + * The distance between the expanded position of the flyout and the collapsed position. + * + * The distance is calculated between the bottom corner which is aligned with the bubble bar. + */ + val distanceToCollapsedPosition: PointF + + /** The size of the flyout when collapsed. */ + val collapsedSize: Float + + /** The color of the flyout when collapsed. */ + val collapsedColor: Int + + /** The elevation of the flyout when collapsed. */ + val collapsedElevation: Float + + /** + * The distance the flyout must pass from its collapsed position until it can start revealing + * the triangle. + */ + val distanceToRevealTriangle: Float +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt new file mode 100644 index 0000000000..ac87b5ee00 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt @@ -0,0 +1,433 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Outline +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.view.LayoutInflater +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.animation.ArgbEvaluator +import com.android.launcher3.R +import com.android.launcher3.popup.RoundedArrowDrawable +import com.android.wm.shell.shared.TypefaceUtils +import com.android.wm.shell.shared.TypefaceUtils.Companion.setTypeface +import kotlin.math.min + +/** The flyout view used to notify the user of a new bubble notification. */ +class BubbleBarFlyoutView( + context: Context, + private val positioner: BubbleBarFlyoutPositioner, + scheduler: FlyoutScheduler? = null, +) : ConstraintLayout(context) { + + companion object { + // the rate multiple for the background color animation relative to the morph animation. + const val BACKGROUND_COLOR_CHANGE_RATE = 5 + // the minimum progress of the expansion animation before the content starts fading in. + private const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f + + private const val TEXT_ROW_HEIGHT_SP = 20 + private const val MAX_ROWS_COUNT = 3 + + /** Returns the maximum possible height of the flyout view. */ + fun getMaximumViewHeight(context: Context): Int { + val verticalPaddings = getFlyoutPadding(context) * 2 + val textSizeSp = TEXT_ROW_HEIGHT_SP * MAX_ROWS_COUNT + val textSizePx = textSizeSp * Resources.getSystem().displayMetrics.scaledDensity + val triangleHeight = + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_height) + return verticalPaddings + textSizePx.toInt() + triangleHeight + } + + private fun getFlyoutPadding(context: Context) = + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding) + } + + private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this) + private val title: TextView by + lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_title) } + + private val icon: ImageView by + lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_icon) } + + private val message: TextView by + lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_text) } + + private val flyoutPadding by lazy(LazyThreadSafetyMode.NONE) { getFlyoutPadding(context) } + + private val triangleHeight by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_height) + } + + private val triangleOverlap by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize( + R.dimen.bubblebar_flyout_triangle_overlap_amount + ) + } + + private val triangleWidth by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_width) + } + + private val triangleRadius by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_triangle_radius) + } + + private val minFlyoutWidth by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_min_width) + } + + private val maxFlyoutWidth by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_max_width) + } + + private val flyoutElevation by + lazy(LazyThreadSafetyMode.NONE) { + context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_elevation).toFloat() + } + + /** The bounds of the background rect. */ + private val backgroundRect = RectF() + private val cornerRadius: Float + private val triangle: Path = Path() + private val triangleOutline = Outline() + private var backgroundColor = Color.BLACK + /** Represents the progress of the expansion animation. 0 when collapsed. 1 when expanded. */ + private var expansionProgress = 0f + /** Translation x-y values to move the flyout to its collapsed position. */ + private var translationToCollapsedPosition = PointF(0f, 0f) + /** The size of the flyout when it's collapsed. */ + private var collapsedSize = 0f + /** The corner radius of the flyout when it's collapsed. */ + private var collapsedCornerRadius = 0f + /** The color of the flyout when collapsed. */ + private var collapsedColor = 0 + /** The elevation of the flyout when collapsed. */ + private var collapsedElevation = 0f + /** The minimum progress of the expansion animation before the triangle is made visible. */ + private var minExpansionProgressForTriangle = 0f + + /** The corner radius of the background according to the progress of the animation. */ + private val currentCornerRadius + get() = collapsedCornerRadius + (cornerRadius - collapsedCornerRadius) * expansionProgress + + /** Translation X of the background. */ + private val backgroundRectTx + get() = translationToCollapsedPosition.x * (1 - expansionProgress) + + /** Translation Y of the background. */ + private val backgroundRectTy + get() = translationToCollapsedPosition.y * (1 - expansionProgress) + + /** + * The paint used to draw the background, whose color changes as the flyout transitions to the + * tinted notification dot. + */ + private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) + + /** The bounds of the flyout relative to the parent view. */ + val bounds = Rect() + + init { + LayoutInflater.from(context).inflate(R.layout.bubblebar_flyout, this, true) + id = R.id.bubble_bar_flyout_view + + setTypeface(title, TypefaceUtils.FontFamily.GSF_LABEL_LARGE) + setTypeface(message, TypefaceUtils.FontFamily.GSF_BODY_MEDIUM) + + val ta = context.obtainStyledAttributes(intArrayOf(android.R.attr.dialogCornerRadius)) + cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat() + ta.recycle() + + setWillNotDraw(false) + clipChildren = true + clipToPadding = false + + val padding = context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding) + // add extra padding to the bottom of the view to include the triangle + setPadding(padding, padding, padding, padding + triangleHeight - triangleOverlap) + translationZ = flyoutElevation + + RoundedArrowDrawable.addDownPointingRoundedTriangleToPath( + triangleWidth.toFloat(), + triangleHeight.toFloat(), + triangleRadius.toFloat(), + triangle, + ) + triangleOutline.setPath(triangle) + + outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + this@BubbleBarFlyoutView.getOutline(outline) + } + } + clipToOutline = true + + applyConfigurationColors(resources.configuration) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + bounds.left = left + bounds.top = top + bounds.right = right + bounds.bottom = bottom + } + + /** Sets the data for the flyout and starts playing the expand animation. */ + fun showFromCollapsed(flyoutMessage: BubbleBarFlyoutMessage, expandAnimation: () -> Unit) { + icon.alpha = 0f + title.alpha = 0f + message.alpha = 0f + setData(flyoutMessage) + + updateTranslationToCollapsedPosition() + collapsedSize = positioner.collapsedSize + collapsedCornerRadius = collapsedSize / 2 + collapsedColor = positioner.collapsedColor + collapsedElevation = positioner.collapsedElevation + + // calculate the expansion progress required before we start showing the triangle as part of + // the expansion animation + minExpansionProgressForTriangle = + positioner.distanceToRevealTriangle / translationToCollapsedPosition.y + + backgroundPaint.color = collapsedColor + + // post the request to start the expand animation to the looper so the view can measure + // itself + scheduler.runAfterLayout(expandAnimation) + } + + /** Updates the content of the flyout and schedules [afterLayout] to run after a layout pass. */ + fun updateData(flyoutMessage: BubbleBarFlyoutMessage, afterLayout: () -> Unit) { + setData(flyoutMessage) + scheduler.runAfterLayout(afterLayout) + } + + private fun setData(flyoutMessage: BubbleBarFlyoutMessage) { + if (flyoutMessage.icon != null) { + icon.visibility = VISIBLE + icon.setImageDrawable(flyoutMessage.icon) + } else { + icon.visibility = GONE + } + + val minTextViewWidth: Int + val maxTextViewWidth: Int + if (icon.visibility == VISIBLE) { + minTextViewWidth = minFlyoutWidth - icon.width - flyoutPadding * 2 + maxTextViewWidth = maxFlyoutWidth - icon.width - flyoutPadding * 2 + } else { + // when there's no avatar, the width of the text view is constant, so we're setting the + // min and max to the same value + minTextViewWidth = minFlyoutWidth - flyoutPadding * 2 + maxTextViewWidth = minTextViewWidth + } + + if (flyoutMessage.title.isEmpty()) { + title.visibility = GONE + } else { + title.minWidth = minTextViewWidth + title.maxWidth = maxTextViewWidth + title.text = flyoutMessage.title + title.visibility = VISIBLE + } + + message.minWidth = minTextViewWidth + message.maxWidth = maxTextViewWidth + message.text = flyoutMessage.message + } + + /** + * This should be called to update [translationToCollapsedPosition] before we start expanding or + * collapsing to make sure that we're animating the flyout to and from the correct position. + */ + fun updateTranslationToCollapsedPosition() { + val txToCollapsedPosition = + if (positioner.isOnLeft) { + positioner.distanceToCollapsedPosition.x + } else { + -positioner.distanceToCollapsedPosition.x + } + val tyToCollapsedPosition = + positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap + translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition) + } + + /** Updates the flyout view with the progress of the animation. */ + fun updateExpansionProgress(fraction: Float) { + expansionProgress = fraction + + updateTranslationForAnimation(message) + updateTranslationForAnimation(title) + updateTranslationForAnimation(icon) + + // start fading in the content only after we're past the threshold + val alpha = + ((expansionProgress - MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA) / + (1f - MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA)) + .coerceIn(0f, 1f) + title.alpha = alpha + message.alpha = alpha + icon.alpha = alpha + + translationZ = + collapsedElevation + (flyoutElevation - collapsedElevation) * expansionProgress + + invalidate() + } + + override fun onDraw(canvas: Canvas) { + // interpolate the width, height, corner radius and translation based on the progress of the + // animation. + // the background is drawn from the bottom left corner to the top right corner if we're + // positioned on the left, and from the bottom right corner to the top left if we're + // positioned on the right. + + // the current width of the background rect according to the progress of the animation + val currentWidth = collapsedSize + (width - collapsedSize) * expansionProgress + val rectBottom = height - triangleHeight + triangleOverlap + val currentHeight = collapsedSize + (rectBottom - collapsedSize) * expansionProgress + + backgroundRect.set( + if (positioner.isOnLeft) 0f else width.toFloat() - currentWidth, + height.toFloat() - triangleHeight + triangleOverlap - currentHeight, + if (positioner.isOnLeft) currentWidth else width.toFloat(), + height.toFloat() - triangleHeight + triangleOverlap, + ) + + // transform the flyout color between the collapsed and expanded states. the color + // transformation completes at a faster rate (BACKGROUND_COLOR_CHANGE_RATE) than the + // expansion animation. this helps make the color change smooth. + backgroundPaint.color = + ArgbEvaluator.getInstance() + .evaluate( + min(expansionProgress * BACKGROUND_COLOR_CHANGE_RATE, 1f), + collapsedColor, + backgroundColor, + ) + + canvas.save() + canvas.translate(backgroundRectTx, backgroundRectTy) + // draw the background starting from the bottom left if we're positioned left, or the bottom + // right if we're positioned right. + canvas.drawRoundRect( + backgroundRect, + currentCornerRadius, + currentCornerRadius, + backgroundPaint, + ) + if (expansionProgress >= minExpansionProgressForTriangle) { + drawTriangle(canvas) + } + canvas.restore() + invalidateOutline() + super.onDraw(canvas) + } + + private fun drawTriangle(canvas: Canvas) { + canvas.save() + val triangleX = + if (positioner.isOnLeft) { + currentCornerRadius + } else { + width - currentCornerRadius - triangleWidth + } + // instead of scaling the triangle, increasingly reveal it from the background. this has the + // effect of the triangle scaling. + + // the translation y of the triangle before we start revealing it. align its bottom with the + // bottom of the rect + val triangleYCollapsed = height - triangleHeight - (triangleHeight - triangleOverlap) + // the translation y of the triangle when it's fully revealed + val triangleYExpanded = height - triangleHeight + val interpolatedExpansion = + ((expansionProgress - minExpansionProgressForTriangle) / + (1 - minExpansionProgressForTriangle)) + .coerceIn(0f, 1f) + val triangleY = + triangleYCollapsed + (triangleYExpanded - triangleYCollapsed) * interpolatedExpansion + canvas.translate(triangleX, triangleY) + canvas.drawPath(triangle, backgroundPaint) + triangleOutline.setPath(triangle) + triangleOutline.offset(triangleX.toInt(), triangleY.toInt()) + canvas.restore() + } + + private fun getOutline(outline: Outline) { + val path = Path() + path.addRoundRect( + backgroundRect, + currentCornerRadius, + currentCornerRadius, + Path.Direction.CW, + ) + if (expansionProgress >= minExpansionProgressForTriangle) { + path.addPath(triangleOutline.mPath) + } + outline.setPath(path) + outline.offset(backgroundRectTx.toInt(), backgroundRectTy.toInt()) + } + + private fun updateTranslationForAnimation(view: View) { + val tx = + if (positioner.isOnLeft) { + translationToCollapsedPosition.x - view.left + } else { + width - view.left - translationToCollapsedPosition.x + } + val ty = height - view.top + translationToCollapsedPosition.y + view.translationX = tx * (1f - expansionProgress) + view.translationY = ty * (1f - expansionProgress) + } + + private fun applyConfigurationColors(configuration: Configuration) { + val nightModeFlags = configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + val isNightModeOn = nightModeFlags == Configuration.UI_MODE_NIGHT_YES + val defaultBackgroundColor = if (isNightModeOn) Color.BLACK else Color.WHITE + val defaultTextColor = if (isNightModeOn) Color.WHITE else Color.BLACK + + backgroundColor = + context.getColor(com.android.internal.R.color.materialColorSurfaceContainer) + title.setTextColor(context.getColor(com.android.internal.R.color.materialColorOnSurface)) + message.setTextColor( + context.getColor(com.android.internal.R.color.materialColorOnSurfaceVariant) + ) + backgroundPaint.color = backgroundColor + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt new file mode 100644 index 0000000000..0804a62d6e --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt @@ -0,0 +1,24 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +/** Callbacks that the flyout uses to notify of events. */ +interface FlyoutCallbacks { + + /** The flyout was clicked. */ + fun flyoutClicked() +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt new file mode 100644 index 0000000000..6f5d700b0f --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt @@ -0,0 +1,32 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.view.View + +/** Interface for scheduling jobs by flyout. */ +fun interface FlyoutScheduler { + /** Runs the given [block] after layout. */ + fun runAfterLayout(block: () -> Unit) +} + +/** A [FlyoutScheduler] that uses a Handler to schedule jobs. */ +class HandlerScheduler(val view: View) : FlyoutScheduler { + override fun runAfterLayout(block: () -> Unit) { + view.post(block) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleBarLocationOnDemandListener.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleBarLocationOnDemandListener.kt new file mode 100644 index 0000000000..ffe7c4473b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleBarLocationOnDemandListener.kt @@ -0,0 +1,34 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +/** On demand implementation of [BubbleBarLocationListener]. */ +class BubbleBarLocationOnDemandListener( + private val listenerProvider: () -> BubbleBarLocationListener +) : BubbleBarLocationListener { + + override fun onBubbleBarLocationAnimated(location: BubbleBarLocation) { + listenerProvider().onBubbleBarLocationAnimated(location) + } + + override fun onBubbleBarLocationUpdated(location: BubbleBarLocation) { + listenerProvider().onBubbleBarLocationUpdated(location) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt new file mode 100644 index 0000000000..56622020e8 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt @@ -0,0 +1,232 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import android.graphics.Rect +import android.view.InsetsController +import android.view.MotionEvent +import android.view.View +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.util.MultiPropertyFactory +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import java.io.PrintWriter + +/** StashController that defines stashing behaviour for the taskbar modes. */ +interface BubbleStashController { + + /** + * Abstraction on the task bar activity context to only provide the dimensions required for + * [BubbleBarView] translation Y computation. + */ + interface TaskbarHotseatDimensionsProvider { + + /** Provides taskbar bottom space in pixels. */ + fun getTaskbarBottomSpace(): Int + + /** Provides taskbar height in pixels. */ + fun getTaskbarHeight(): Int + } + + /** Execute passed action only after controllers are initiated. */ + interface ControllersAfterInitAction { + /** Execute action after controllers are initiated. */ + fun runAfterInit(action: Runnable) + } + + /** Launcher states bubbles cares about */ + enum class BubbleLauncherState { + /* When launcher is in overview */ + OVERVIEW, + /* When launcher is on home */ + HOME, + /* We're in an app */ + IN_APP, + } + + /** The current launcher state */ + var launcherState: BubbleLauncherState + + /** Whether bubble bar is currently stashed */ + val isStashed: Boolean + + /** Whether launcher enters or exits the home page. */ + val isBubblesShowingOnHome: Boolean + get() = launcherState == BubbleLauncherState.HOME + + /** Whether launcher enters or exits the overview page. */ + val isBubblesShowingOnOverview: Boolean + get() = launcherState == BubbleLauncherState.OVERVIEW + + /** Bubble bar vertical center for launcher home. */ + var bubbleBarVerticalCenterForHome: Int + + /** Updated when sysui locked state changes, when locked, bubble bar is not shown. */ + var isSysuiLocked: Boolean + + /** Whether there is a transient taskbar mode */ + val isTransientTaskBar: Boolean + + /** Whether stash control has a handle view */ + val hasHandleView: Boolean + + /** Initialize controller */ + fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: ControllersAfterInitAction, + ) + + /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */ + fun showBubbleBarImmediate() + + /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */ + fun showBubbleBarImmediate(bubbleBarTranslationY: Float) + + /** Stashes the bubble bar immediately without animation. */ + fun stashBubbleBarImmediate() + + /** Returns the touchable height of the bubble bar based on it's stashed state. */ + fun getTouchableHeight(): Int + + /** Whether bubble bar is currently visible */ + fun isBubbleBarVisible(): Boolean + + /** + * Updates the values of the internal animators after the new bubble animation was interrupted + * + * @param isStashed whether the current state should be stashed + * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the + * bubble bar is showing to ensure that the stash animator runs smoothly. + */ + fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) + + /** Checks whether the motion event is over the stash handle or bubble bar. */ + fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean + + /** Set a bubble bar location */ + fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) + + /** + * Stashes the bubble bar (transform to the handle view), or just shrink width of the expanded + * bubble bar based on the controller implementation. + */ + fun stashBubbleBar() + + /** + * Animates the bubble bar to the handle at provided location. Does not update bubble bar + * location. + */ + fun stashBubbleBarToLocation(fromLocation: BubbleBarLocation, toLocation: BubbleBarLocation) {} + + /** Shows the bubble bar, and expands bubbles depending on [expandBubbles]. */ + fun showBubbleBar(expandBubbles: Boolean) { + showBubbleBar(expandBubbles = expandBubbles, bubbleBarGesture = false) + } + + /** + * Shows the bubble bar, and expands bubbles depending on [expandBubbles]. + * + * Set [bubbleBarGesture] to true if this request originates from a touch gesture on the bubble + * bar. + */ + fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) + + /** Animates the bubble bar at the provided location. Does not update bubble bar location. */ + fun showBubbleBarAtLocation(fromLocation: BubbleBarLocation, toLocation: BubbleBarLocation) {} + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** + * The difference on the Y axis between the center of the handle and the center of the bubble + * bar. + */ + fun getDiffBetweenHandleAndBarCenters(): Float + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** The distance the handle moves as part of the new bubble animation. */ + fun getStashedHandleTranslationForNewBubbleAnimation(): Float + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** Returns the [PhysicsAnimator] for the stashed handle view. */ + fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** Notifies taskbar that it should update its touchable region. */ + fun updateTaskbarTouchRegion() + + // TODO(b/354218264): Move to BubbleBarViewAnimator + /** Set the translation Y for the stashed handle. */ + fun setHandleTranslationY(translationY: Float) + + /** Returns the translation of the handle. */ + fun getHandleTranslationY(): Float? + + /** Returns bounds of the handle */ + fun getHandleBounds(bounds: Rect) + + /** Returns MultiValueAlpha of the handle view when the handle view is shown. */ + fun getHandleViewAlpha(): MultiPropertyFactory.MultiProperty? = null + + /** + * Default implementation only analyse [isBubblesShowingOnHome] and return value is equal to + * [targetTranslationYForState]. + */ + val bubbleBarTranslationY: Float + get() = targetTranslationYForState + + /** Returns bubble bar Y target position according to [isBubblesShowingOnHome] value. */ + val targetTranslationYForState: Float + get() = + if (isBubblesShowingOnHome) { + bubbleBarTranslationYForHotseat + } else { + bubbleBarTranslationYForTaskbar + } + + /** Translation Y to align the bubble bar with the taskbar. */ + val bubbleBarTranslationYForTaskbar: Float + + /** Return translation Y to align the bubble bar with the hotseat. */ + val bubbleBarTranslationYForHotseat: Float + + /** + * Show bubble bar is if it were in-app while launcher state is still on home. Set as a progress + * value between 0 and 1: 0 - use home layout, 1 - use in-app layout. + */ + var inAppDisplayOverrideProgress: Float + + /** Dumps the state of BubbleStashController. */ + fun dump(pw: PrintWriter) { + pw.println("Bubble stash controller state:") + pw.println(" isStashed: $isStashed") + pw.println(" isBubblesShowingOnOverview: $isBubblesShowingOnOverview") + pw.println(" isBubblesShowingOnHome: $isBubblesShowingOnHome") + pw.println(" isSysuiLocked: $isSysuiLocked") + } + + companion object { + /** How long to stash/unstash. */ + const val BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE.toLong() + + /** How long to translate Y coordinate of the BubbleBar. */ + const val BAR_TRANSLATION_DURATION = 300L + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt new file mode 100644 index 0000000000..2408274c9f --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt @@ -0,0 +1,35 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import com.android.launcher3.DeviceProfile +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider + +/** + * Implementation of the [TaskbarHotseatDimensionsProvider] that take sizes from the + * [DeviceProfile]. + */ +class DeviceProfileDimensionsProviderAdapter( + private val taskbarActivityContext: TaskbarActivityContext +) : TaskbarHotseatDimensionsProvider { + override fun getTaskbarBottomSpace(): Int = taskbarDp().taskbarProfile.bottomMargin + + override fun getTaskbarHeight(): Int = taskbarDp().taskbarProfile.height + + private fun taskbarDp(): DeviceProfile = taskbarActivityContext.deviceProfile +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt new file mode 100644 index 0000000000..5c8746cb8b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt @@ -0,0 +1,274 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import com.android.app.animation.Interpolators +import com.android.launcher3.Utilities +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider +import com.android.launcher3.util.MultiPropertyFactory +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +class PersistentBubbleStashController( + private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider +) : BubbleStashController { + + private lateinit var taskbarInsetsController: TaskbarInsetsController + private lateinit var bubbleBarViewController: BubbleBarViewController + private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat + private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory.MultiProperty + private lateinit var bubbleBarScaleAnimator: AnimatedFloat + private lateinit var controllersAfterInitAction: ControllersAfterInitAction + override var bubbleBarVerticalCenterForHome: Int = 0 + + override var launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP + set(state) { + if (field == state) return + val transitionFromHome = field == BubbleLauncherState.HOME + field = state + val hasBubbles = bubbleBarViewController.hasBubbles() + bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles) + if (!hasBubbles) { + // if there are no bubbles, there's nothing to show, so just return. + return + } + if (transitionFromHome && inAppDisplayOverrideProgress != 0f) { + // was on -1 page and leaving it, - reset the inAppDisplayOverrideProgress + inAppDisplayOverrideProgress = 0f + } + // If we're transitioning anywhere, bubble bar should be collapsed + updateExpandedState(expand = false) + if (transitionFromHome || field == BubbleLauncherState.HOME) { + // If we're transitioning to or from home, animate the Y because we're in hotseat + // on home but in persistent taskbar elsewhere so the position is different. + animateBubbleBarY() + } + } + + override var isSysuiLocked: Boolean = false + set(isLocked) { + if (field == isLocked) return + field = isLocked + if (!isLocked && bubbleBarViewController.hasBubbles()) { + animateAfterUnlock() + } + } + + override var isTransientTaskBar: Boolean = false + + /** When the bubble bar is shown for the persistent task bar, there is no handle view. */ + override val hasHandleView: Boolean = false + + /** For persistent task bar we never stash the bubble bar */ + override val isStashed: Boolean = false + + override val bubbleBarTranslationYForTaskbar: Float + get() { + val taskbarBottomMargin = taskbarHotseatDimensionsProvider.getTaskbarBottomSpace() + val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight + val taskbarHeight = taskbarHotseatDimensionsProvider.getTaskbarHeight() + return -taskbarBottomMargin - (taskbarHeight - bubbleBarHeight) / 2f + } + + override val bubbleBarTranslationYForHotseat: Float + get() { + val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight + return -bubbleBarVerticalCenterForHome + bubbleBarHeight / 2 + } + + /** + * Returns bubble bar Y target position according to [isBubblesShowingOnHome] value. Value could + * be adjusted to the display override progress. + */ + override val bubbleBarTranslationY: Float + get() = + if (inAppDisplayOverrideProgress > 0f && launcherState == BubbleLauncherState.HOME) { + Utilities.mapToRange( + inAppDisplayOverrideProgress, + /* fromMin = */ 0f, + /* fromMax = */ 1f, + bubbleBarTranslationYForHotseat, + bubbleBarTranslationYForTaskbar, + Interpolators.LINEAR, + ) + } else { + targetTranslationYForState + } + + override var inAppDisplayOverrideProgress: Float = 0f + set(value) { + if (field == value) return + field = value + if (launcherState == BubbleLauncherState.HOME) { + if (bubbleBarTranslationYAnimator.isAnimating) { + bubbleBarTranslationYAnimator.cancelAnimation() + } + bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY) + if (value == 0f || value == 1f) { + // Update insets only when we reach the end values + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + } + } + + override fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: ControllersAfterInitAction, + ) { + this.taskbarInsetsController = taskbarInsetsController + this.bubbleBarViewController = bubbleBarViewController + this.controllersAfterInitAction = controllersAfterInitAction + bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY + // bubble bar has only alpha property, getting it at index 0 + bubbleBarAlphaAnimator = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0) + bubbleBarScaleAnimator = bubbleBarViewController.bubbleBarScaleY + } + + private fun animateAfterUnlock() { + val animatorSet = AnimatorSet() + if (isBubblesShowingOnHome || isBubblesShowingOnOverview) { + animatorSet.playTogether( + bubbleBarScaleAnimator.animateToValue(1f), + bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY), + bubbleBarAlphaAnimator.animateToValue(1f), + ) + } + updateTouchRegionOnAnimationEnd(animatorSet) + animatorSet.setDuration(BAR_STASH_DURATION).start() + } + + override fun showBubbleBarImmediate() = showBubbleBarImmediate(bubbleBarTranslationY) + + override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) { + bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY) + bubbleBarAlphaAnimator.setValue(1f) + bubbleBarScaleAnimator.updateValue(1f) + } + + override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) { + // When the bubble bar is shown for the persistent task bar, there is no handle view, so no + // operation is performed. + } + + override fun stashBubbleBar() { + updateExpandedState(expand = false) + } + + override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) { + updateExpandedState(expand = expandBubbles, bubbleBarGesture = bubbleBarGesture) + } + + override fun stashBubbleBarImmediate() { + // When the bubble bar is shown for the persistent task bar, there is no handle view, so no + // operation is performed. + } + + /** If bubble bar is visible return bubble bar height, 0 otherwise */ + override fun getTouchableHeight() = + if (isBubbleBarVisible()) { + bubbleBarViewController.bubbleBarCollapsedHeight.toInt() + } else { + 0 + } + + override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles() + + override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) { + showBubbleBarImmediate(bubbleBarTranslationY) + } + + override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean = + bubbleBarViewController.isEventOverAnyItem(ev) + + override fun getDiffBetweenHandleAndBarCenters(): Float { + // distance from the bottom of the screen and the bubble bar center. + return -bubbleBarViewController.bubbleBarCollapsedHeight / 2f + } + + /** When the bubble bar is shown for the persistent task bar, there is no handle view. */ + override fun getStashedHandleTranslationForNewBubbleAnimation(): Float = 0f + + /** When the bubble bar is shown for the persistent task bar, there is no handle view. */ + override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? = null + + override fun updateTaskbarTouchRegion() { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + /** + * When the bubble bar is shown for the persistent task bar the bar does not stash, so no + * operation is performed + */ + override fun setHandleTranslationY(translationY: Float) { + // no op since does not have a handle view + } + + override fun getHandleTranslationY(): Float? = null + + override fun getHandleBounds(bounds: Rect) { + // no op since does not have a handle view + } + + private fun updateExpandedState(expand: Boolean, bubbleBarGesture: Boolean = false) { + if (bubbleBarViewController.isHiddenForNoBubbles) { + // If there are no bubbles the bar is invisible, nothing to do here. + return + } + if (bubbleBarViewController.isExpanded != expand) { + val maybeShowEdu = expand && bubbleBarGesture + bubbleBarViewController.animateExpanded(expand, maybeShowEdu) + } + } + + /** Animates bubble bar Y accordingly to the showing mode */ + private fun animateBubbleBarY() { + val animator = + bubbleBarViewController.bubbleBarTranslationY.animateToValue(bubbleBarTranslationY) + updateTouchRegionOnAnimationEnd(animator) + animator.setDuration(BAR_TRANSLATION_DURATION) + animator.start() + } + + private fun updateTouchRegionOnAnimationEnd(animator: Animator) { + animator.addListener( + object : AnimatorListenerAdapter() { + + override fun onAnimationEnd(animation: Animator) { + controllersAfterInitAction.runAfterInit { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + } + } + ) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt new file mode 100644 index 0000000000..00e79029e2 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt @@ -0,0 +1,684 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnEnd +import androidx.core.animation.doOnStart +import androidx.dynamicanimation.animation.SpringForce +import com.android.app.animation.Interpolators.EMPHASIZED +import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.R +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.anim.SpringAnimationBuilder +import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_IN_ANIM_ALPHA_DURATION_MS +import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_OUT_ANIM_ALPHA_DELAY_MS +import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_OUT_ANIM_ALPHA_DURATION_MS +import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_OUT_ANIM_POSITION_DURATION_MS +import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.inShiftX +import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.outShift +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY +import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider +import com.android.launcher3.util.MultiPropertyFactory +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.ContextUtils.isRtl +import kotlin.math.max + +class TransientBubbleStashController( + private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider, + private val context: Context, +) : BubbleStashController { + + private lateinit var bubbleBarViewController: BubbleBarViewController + private lateinit var taskbarInsetsController: TaskbarInsetsController + private lateinit var controllersAfterInitAction: ControllersAfterInitAction + + // stash view properties + private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null + private var stashHandleViewAlpha: MultiPropertyFactory.MultiProperty? = null + private var translationYDuringStash = AnimatedFloat { transY -> + bubbleStashedHandleViewController?.setTranslationYForStash(transY) + bubbleBarViewController.setTranslationYForStash(transY) + } + private val stashHandleStashVelocity = + context.resources.getDimension(R.dimen.bubblebar_stashed_handle_spring_velocity_dp_per_s) + private var stashedHeight: Int = 0 + + // bubble bar properties + private lateinit var bubbleBarAlpha: MultiPropertyFactory.MultiProperty + private lateinit var bubbleBarBubbleAlpha: AnimatedFloat + private lateinit var bubbleBarBackgroundAlpha: AnimatedFloat + private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat + private lateinit var bubbleBarBubbleTranslationY: AnimatedFloat + private lateinit var bubbleBarBackgroundScaleX: AnimatedFloat + private lateinit var bubbleBarBackgroundScaleY: AnimatedFloat + private val handleCenterFromScreenBottom = + context.resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f + + private var animator: AnimatorSet? = null + override var bubbleBarVerticalCenterForHome: Int = 0 + + override var isStashed: Boolean = false + @VisibleForTesting set + + override var launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP + set(state) { + if (field == state) return + field = state + val hasBubbles = bubbleBarViewController.hasBubbles() + bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles) + if (!hasBubbles) { + // if there are no bubbles, there's no need to update the bubble bar, just keep the + // isStashed state up to date so that we can process state changes when bubbles are + // created. + isStashed = launcherState == BubbleLauncherState.IN_APP + return + } + if (field == BubbleLauncherState.HOME) { + // When to home we need to animate the bubble bar + // here to align with hotseat center. + animateBubbleBarYToHotseat() + } else if (field == BubbleLauncherState.OVERVIEW) { + // When transitioning to overview we need to animate the bubble bar to align with + // the taskbar bottom. + animateBubbleBarYToTaskbar() + } + // Only stash if we're in an app, otherwise we're in home or overview where we should + // be un-stashed + val stash = field == BubbleLauncherState.IN_APP + val expand = + if (stash) { + // Always collapse when we are stashing + false + } else { + // If unstashing, keep the current state + bubbleBarViewController.isExpanded + } + updateStashedAndExpandedState(stash, expand) + } + + override var isSysuiLocked: Boolean = false + set(isLocked) { + if (field == isLocked) return + field = isLocked + if (!isLocked && bubbleBarViewController.hasBubbles()) { + animateAfterUnlock() + } + } + + override val isTransientTaskBar: Boolean = true + + override val bubbleBarTranslationYForHotseat: Float + get() { + val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight + return -bubbleBarVerticalCenterForHome + bubbleBarHeight / 2 + } + + override val bubbleBarTranslationYForTaskbar: Float = + -taskbarHotseatDimensionsProvider.getTaskbarBottomSpace().toFloat() + + /** Not supported in transient mode */ + override var inAppDisplayOverrideProgress: Float = 0f + + /** Check if we have handle view controller */ + override val hasHandleView: Boolean + get() = bubbleStashedHandleViewController != null + + override fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: ControllersAfterInitAction, + ) { + this.taskbarInsetsController = taskbarInsetsController + this.bubbleBarViewController = bubbleBarViewController + this.bubbleStashedHandleViewController = bubbleStashedHandleViewController + this.controllersAfterInitAction = controllersAfterInitAction + bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY + bubbleBarBubbleTranslationY = bubbleBarViewController.bubbleOffsetY + // bubble bar has only alpha property, getting it at index 0 + bubbleBarAlpha = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0) + bubbleBarBubbleAlpha = bubbleBarViewController.bubbleBarBubbleAlpha + bubbleBarBackgroundAlpha = bubbleBarViewController.bubbleBarBackgroundAlpha + bubbleBarBackgroundScaleX = bubbleBarViewController.bubbleBarBackgroundScaleX + bubbleBarBackgroundScaleY = bubbleBarViewController.bubbleBarBackgroundScaleY + stashedHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0 + stashHandleViewAlpha = bubbleStashedHandleViewController?.stashedHandleAlpha?.get(0) + } + + private fun animateAfterUnlock() { + val animatorSet = AnimatorSet() + if (isBubblesShowingOnHome || isBubblesShowingOnOverview) { + isStashed = false + animatorSet.playTogether( + bubbleBarBackgroundScaleX.animateToValue(1f), + bubbleBarBackgroundScaleY.animateToValue(1f), + bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY), + bubbleBarAlpha.animateToValue(1f), + bubbleBarBubbleAlpha.animateToValue(1f), + bubbleBarBackgroundAlpha.animateToValue(1f), + ) + } else { + isStashed = true + stashHandleViewAlpha?.let { animatorSet.playTogether(it.animateToValue(1f)) } + } + animatorSet + .updateBarVisibility(isStashed) + .updateTouchRegionOnAnimationEnd() + .setDuration(BAR_STASH_DURATION) + .start() + } + + override fun showBubbleBarImmediate() { + showBubbleBarImmediate(bubbleBarTranslationY) + } + + override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) { + showBubbleBarImmediateVisually(bubbleBarTranslationY) + onIsStashedChanged() + } + + private fun showBubbleBarImmediateVisually(bubbleBarTranslationY: Float) { + bubbleStashedHandleViewController?.setTranslationYForSwipe(0f) + stashHandleViewAlpha?.value = 0f + this.bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY) + bubbleBarAlpha.setValue(1f) + bubbleBarBubbleAlpha.updateValue(1f) + bubbleBarBackgroundAlpha.updateValue(1f) + bubbleBarBackgroundScaleX.updateValue(1f) + bubbleBarBackgroundScaleY.updateValue(1f) + isStashed = false + bubbleBarViewController.setHiddenForStashed(false) + } + + override fun stashBubbleBarImmediate() { + stashBubbleBarImmediateVisually() + onIsStashedChanged() + } + + private fun stashBubbleBarImmediateVisually() { + bubbleStashedHandleViewController?.setTranslationYForSwipe(0f) + stashHandleViewAlpha?.value = 1f + this.bubbleBarTranslationYAnimator.updateValue(getStashTranslation()) + bubbleBarAlpha.setValue(0f) + // Reset bubble and background alpha to 1 and only keep the bubble bar alpha at 0 + bubbleBarBubbleAlpha.updateValue(1f) + bubbleBarBackgroundAlpha.updateValue(1f) + bubbleBarBackgroundScaleX.updateValue(getStashScaleX()) + bubbleBarBackgroundScaleY.updateValue(getStashScaleY()) + isStashed = true + bubbleBarViewController.setHiddenForStashed(true) + } + + override fun getTouchableHeight(): Int = + when { + isStashed -> stashedHeight + isBubbleBarVisible() -> bubbleBarViewController.bubbleBarCollapsedHeight.toInt() + else -> 0 + } + + override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles() && !isStashed + + override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) { + if (isStashed) { + stashBubbleBarImmediate() + } else { + showBubbleBarImmediate(bubbleBarTranslationY) + } + } + + /** Check if [ev] belongs to the stash handle or the bubble bar views. */ + override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean { + val isOverHandle = bubbleStashedHandleViewController?.isEventOverHandle(ev) ?: false + return isOverHandle || bubbleBarViewController.isEventOverAnyItem(ev) + } + + /** Set the bubble bar stash handle location . */ + override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) { + bubbleStashedHandleViewController?.setBubbleBarLocation(bubbleBarLocation) + } + + override fun stashBubbleBar() { + updateStashedAndExpandedState(stash = true, expand = false) + } + + override fun stashBubbleBarToLocation( + fromLocation: BubbleBarLocation, + toLocation: BubbleBarLocation, + ) { + if (fromLocation.isSameSideWith(toLocation)) { + updateStashedAndExpandedState( + stash = true, + expand = false, + updateTouchRegionOnEnd = false, + ) + return + } + cancelAnimation() + animator = + AnimatorSet().apply { + playSequentially( + bubbleBarViewController.animateBubbleBarLocationOut(toLocation), + createHandleInAnimator(location = toLocation), + ) + start() + } + } + + override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) { + updateStashedAndExpandedState( + stash = false, + expand = expandBubbles, + bubbleBarGesture = bubbleBarGesture, + ) + } + + override fun showBubbleBarAtLocation( + fromLocation: BubbleBarLocation, + toLocation: BubbleBarLocation, + ) { + if (fromLocation.isSameSideWith(toLocation)) { + updateStashedAndExpandedState( + stash = false, + expand = false, + updateTouchRegionOnEnd = false, + ) + return + } + cancelAnimation() + val bubbleBarInAnimation = + bubbleBarViewController.animateBubbleBarLocationIn(fromLocation, toLocation).apply { + doOnStart { showBubbleBarImmediateVisually(bubbleBarTranslationY) } + } + animator = + AnimatorSet().apply { + playSequentially( + createHandleOutAnimator(location = toLocation), + bubbleBarInAnimation, + ) + start() + } + } + + override fun getDiffBetweenHandleAndBarCenters(): Float { + // the difference between the centers of the handle and the bubble bar is the difference + // between their distance from the bottom of the screen. + val barCenter: Float = bubbleBarViewController.bubbleBarCollapsedHeight / 2f + return handleCenterFromScreenBottom - barCenter + } + + override fun getStashedHandleTranslationForNewBubbleAnimation(): Float { + return -handleCenterFromScreenBottom + } + + override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? { + return bubbleStashedHandleViewController?.physicsAnimator + } + + override fun updateTaskbarTouchRegion() { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + override fun setHandleTranslationY(translationY: Float) { + bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY) + } + + override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY + + override fun getHandleBounds(bounds: Rect) { + bubbleStashedHandleViewController?.getBounds(bounds) + } + + private fun getStashTranslation(): Float { + return (bubbleBarTranslationY - stashedHeight) / 2f + } + + @VisibleForTesting + fun getStashScaleX(): Float { + val handleWidth = bubbleStashedHandleViewController?.stashedWidth ?: 0 + return handleWidth / bubbleBarViewController.bubbleBarCollapsedWidth + } + + @VisibleForTesting + fun getStashScaleY(): Float { + val handleHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0 + return handleHeight / bubbleBarViewController.bubbleBarCollapsedHeight + } + + /** + * Create a stash animation. + * + * @param isStashed whether it's a stash animation or an unstash animation + * @param duration duration of the animation + * @return the animation + */ + @Suppress("SameParameterValue") + private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet { + val animatorSet = AnimatorSet() + + animatorSet.play( + createBackgroundAlphaAnimator(isStashed).apply { + val alphaDuration = + if (isStashed) duration else TRANSIENT_TASKBAR_STASH_ALPHA_DURATION + val alphaDelay = if (isStashed) TASKBAR_STASH_ALPHA_START_DELAY else 0L + this.duration = max(0L, alphaDuration - alphaDelay) + this.startDelay = alphaDelay + this.interpolator = LINEAR + } + ) + + animatorSet.play( + bubbleBarBubbleAlpha + .animateToValue(getBarAlphaStart(isStashed), getBarAlphaEnd(isStashed)) + .apply { + this.duration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION + this.startDelay = TASKBAR_STASH_ALPHA_START_DELAY + this.interpolator = LINEAR + } + ) + + animatorSet.play( + createSpringOnStashAnimator(isStashed).apply { + this.duration = duration + this.interpolator = LINEAR + } + ) + + animatorSet.play( + bubbleBarViewController.createRevealAnimatorForStashChange(isStashed).apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + // Animate bubble translation to keep reveal animation in the bounds of the bar + val bubbleTyStart = if (isStashed) 0f else -bubbleBarTranslationY + val bubbleTyEnd = if (isStashed) -bubbleBarTranslationY else 0f + animatorSet.play( + bubbleBarBubbleTranslationY.animateToValue(bubbleTyStart, bubbleTyEnd).apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + animatorSet.play( + bubbleStashedHandleViewController?.createRevealAnimToIsStashed(isStashed)?.apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + val pivotX = if (bubbleBarViewController.isBubbleBarOnLeft) 0f else 1f + animatorSet.play( + createScaleAnimator(isStashed).apply { + this.duration = duration + this.interpolator = EMPHASIZED + this.setBubbleBarPivotDuringAnim(pivotX, 1f) + } + ) + + val translationYTarget = if (isStashed) getStashTranslation() else bubbleBarTranslationY + animatorSet.play( + bubbleBarTranslationYAnimator.animateToValue(translationYTarget).apply { + this.duration = duration + this.interpolator = EMPHASIZED + } + ) + + animatorSet.doOnStart { + // Update the start value for bubble view and background alpha when the entire animation + // begins. + // Alpha animation has a delay, and if we set the initial values at the start of the + // alpha animation, it will cause flickers. + bubbleBarBubbleAlpha.updateValue(getBarAlphaStart(isStashed)) + bubbleBarBackgroundAlpha.updateValue(getBarAlphaStart(isStashed)) + // We animate alpha for background and bubble views separately. Make sure the container + // is always visible. + bubbleBarAlpha.value = 1f + } + animatorSet.doOnEnd { + cancelAnimation() + controllersAfterInitAction.runAfterInit { + if (isStashed) { + bubbleBarAlpha.value = 0f + // reset bubble view alpha + bubbleBarBubbleAlpha.updateValue(1f) + bubbleBarBackgroundAlpha.updateValue(1f) + // reset stash translation + translationYDuringStash.updateValue(0f) + bubbleBarBubbleTranslationY.updateValue(0f) + bubbleBarViewController.animateExpanded(false) + } + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + } + return animatorSet + } + + private fun createBackgroundAlphaAnimator(isStashed: Boolean): AnimatorSet { + return AnimatorSet().apply { + play( + bubbleBarBackgroundAlpha.animateToValue( + getBarAlphaStart(isStashed), + getBarAlphaEnd(isStashed), + ) + ) + play(stashHandleViewAlpha?.animateToValue(getHandleAlphaEnd(isStashed))) + } + } + + private fun getBarAlphaStart(isStashed: Boolean): Float { + return if (isStashed) 1f else 0f + } + + private fun getBarAlphaEnd(isStashed: Boolean): Float { + return if (isStashed) 0f else 1f + } + + private fun getHandleAlphaEnd(isStashed: Boolean): Float { + return if (isStashed) 1f else 0f + } + + private fun createSpringOnStashAnimator(isStashed: Boolean): Animator { + if (!isStashed) { + // Animate the stash translation back to 0 + return translationYDuringStash.animateToValue(0f) + } + // Apply a spring to the handle + return SpringAnimationBuilder(context) + .setStartValue(translationYDuringStash.value) + .setEndValue(0f) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + .setStartVelocity(stashHandleStashVelocity) + .build(translationYDuringStash, AnimatedFloat.VALUE) + } + + private fun createScaleAnimator(isStashed: Boolean): AnimatorSet { + val scaleXTarget = if (isStashed) getStashScaleX() else 1f + val scaleYTarget = if (isStashed) getStashScaleY() else 1f + return AnimatorSet().apply { + play(bubbleBarBackgroundScaleX.animateToValue(scaleXTarget)) + play(bubbleBarBackgroundScaleY.animateToValue(scaleYTarget)) + } + } + + private fun onIsStashedChanged() { + controllersAfterInitAction.runAfterInit { + taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + bubbleStashedHandleViewController?.onIsStashedChanged() + } + } + + private fun animateBubbleBarYToHotseat() { + translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForHotseat) + } + + private fun animateBubbleBarYToTaskbar() { + translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForTaskbar) + } + + private fun translateBubbleBarYUpdateTouchRegionOnCompletion(toY: Float) { + bubbleBarViewController.bubbleBarTranslationY + .animateToValue(toY) + .updateTouchRegionOnAnimationEnd() + .setDuration(BAR_TRANSLATION_DURATION) + .start() + } + + @VisibleForTesting + fun updateStashedAndExpandedState( + stash: Boolean, + expand: Boolean, + bubbleBarGesture: Boolean = false, + updateTouchRegionOnEnd: Boolean = true, + ) { + if (bubbleBarViewController.isHiddenForNoBubbles) { + // If there are no bubbles the bar and handle are invisible, nothing to do here. + cancelAnimation() + return + } + val isStashed = stash && !isBubblesShowingOnHome && !isBubblesShowingOnOverview + if (this.isStashed != isStashed) { + this.isStashed = isStashed + + // notify the view controller that the stash state is about to change so that it can + // cancel an ongoing animation if there is one. + bubbleBarViewController.onStashStateChanging() + cancelAnimation() + animator = + createStashAnimator(isStashed, BAR_STASH_DURATION).apply { + updateBarVisibility(isStashed) + if (updateTouchRegionOnEnd) { + updateTouchRegionOnAnimationEnd() + } + start() + } + } + if (bubbleBarViewController.isExpanded != expand) { + val maybeShowEdu = expand && bubbleBarGesture + bubbleBarViewController.animateExpanded(expand, maybeShowEdu) + } + } + + private fun cancelAnimation() { + animator?.cancel() + animator = null + } + + override fun getHandleViewAlpha(): MultiPropertyFactory.MultiProperty? = + // only return handle alpha if the bubble bar is stashed and has bubbles + if (isStashed && bubbleBarViewController.hasBubbles()) { + stashHandleViewAlpha + } else { + null + } + + private fun Animator.updateTouchRegionOnAnimationEnd(): Animator { + doOnEnd { onIsStashedChanged() } + return this + } + + private fun T.updateBarVisibility(stashed: Boolean): T { + if (stashed) { + doOnEnd { bubbleBarViewController.setHiddenForStashed(true) } + } else { + doOnStart { bubbleBarViewController.setHiddenForStashed(false) } + } + return this + } + + // TODO(b/399678274) add tests + private fun createHandleInAnimator(location: BubbleBarLocation): Animator? { + val stashHandleViewController = bubbleStashedHandleViewController ?: return null + val handleAlpha = stashHandleViewController.stashedHandleAlpha.get(0) + val shift = context.inShiftX + val startX = if (location.isOnLeft(context.isRtl)) shift else -shift + val handleTranslationX = + ValueAnimator.ofFloat(startX, 0f).apply { + addUpdateListener { v -> + stashHandleViewController.setTranslationX(v.animatedValue as Float) + } + duration = FADE_IN_ANIM_ALPHA_DURATION_MS + } + val handleAlphaAnimation = + handleAlpha.animateToValue(1f).apply { duration = FADE_IN_ANIM_ALPHA_DURATION_MS } + return AnimatorSet().apply { + playTogether(handleTranslationX, handleAlphaAnimation) + doOnStart { stashBubbleBarImmediateVisually() } + } + } + + private fun createHandleOutAnimator(location: BubbleBarLocation): Animator? { + val stashHandleViewController = bubbleStashedHandleViewController ?: return null + val handleAlpha = stashHandleViewController.stashedHandleAlpha.get(0) + val shift = context.outShift + val endX = if (location.isOnLeft(context.isRtl)) -shift else shift + val handleTranslationX = + ValueAnimator.ofFloat(0f, endX).apply { + addUpdateListener { v -> + stashHandleViewController.setTranslationX(v.animatedValue as Float) + } + duration = FADE_OUT_ANIM_POSITION_DURATION_MS + // in case item dropped to the opposite side - need to clear translation + doOnEnd { stashHandleViewController.setTranslationX(0f) } + } + val handleAlphaAnimation = + handleAlpha.animateToValue(0f).apply { + duration = FADE_OUT_ANIM_ALPHA_DURATION_MS + startDelay = FADE_OUT_ANIM_ALPHA_DELAY_MS + } + return AnimatorSet().apply { playTogether(handleTranslationX, handleAlphaAnimation) } + } + + private fun Animator.setBubbleBarPivotDuringAnim(pivotX: Float, pivotY: Float): Animator { + var initialPivotX = Float.NaN + var initialPivotY = Float.NaN + doOnStart { + initialPivotX = bubbleBarViewController.bubbleBarRelativePivotX + initialPivotY = bubbleBarViewController.bubbleBarRelativePivotY + bubbleBarViewController.setBubbleBarRelativePivot(pivotX, pivotY) + } + doOnEnd { + if (!initialPivotX.isNaN() && !initialPivotY.isNaN()) { + bubbleBarViewController.setBubbleBarRelativePivot(initialPivotX, initialPivotY) + } + } + return this + } + + private fun BubbleBarLocation.isSameSideWith(anotherLocation: BubbleBarLocation): Boolean { + val isRtl = context.isRtl + return this.isOnLeft(isRtl) == anotherLocation.isOnLeft(isRtl) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt b/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt new file mode 100644 index 0000000000..e384586b45 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/CustomizableTaskbarView.kt @@ -0,0 +1,40 @@ +/* + * 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.launcher3.taskbar.customization + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.launcher3.Insettable +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.views.ActivityContext + +/** TaskbarView that is customizeable via Taskbar containers. */ +class CustomizableTaskbarView(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs), Insettable { + private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) + + init { + inflate(context, R.layout.customizable_taskbar_view, this) + } + + override fun setInsets(insets: Rect?) { + // Ignore, we just implement Insettable to draw behind system insets. + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt new file mode 100644 index 0000000000..2807668107 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt @@ -0,0 +1,182 @@ +/* + * 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.launcher3.taskbar.customization + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color.TRANSPARENT +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import androidx.annotation.DimenRes +import androidx.annotation.DrawableRes +import androidx.core.view.setPadding +import com.android.launcher3.R +import com.android.launcher3.Utilities.dpToPx +import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarViewCallbacks +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.views.ActivityContext +import com.android.launcher3.views.IconButtonView +import com.android.quickstep.DeviceConfigWrapper +import com.android.quickstep.util.ContextualSearchStateManager +import com.android.wm.shell.Flags + +/** Taskbar all apps button container for customizable taskbar. */ +class TaskbarAllAppsButtonContainer +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + IconButtonView(context, attrs), TaskbarContainer { + + private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) + private var allAppsTouchTriggered = false + private var allAppsTouchRunnable: Runnable? = null + private var allAppsButtonTouchDelayMs: Long = ViewConfiguration.getLongPressTimeout().toLong() + private var isTaskbarInMinimalState = false + private lateinit var taskbarViewCallbacks: TaskbarViewCallbacks + + override val spaceNeeded: Int + get() { + return dpToPx( + activityContext.taskbarSpecsEvaluator.taskbarIconSize.size.toFloat(), + activityContext, + ) + } + + init { + contentDescription = context.getString(R.string.all_apps_button_label) + setUpIcon() + } + + @SuppressLint("UseCompatLoadingForDrawables", "ResourceAsColor") + private fun setUpIcon() { + val drawable = + resources.getDrawable( + getAllAppsButton(activityContext.taskbarFeatureEvaluator.isTransient) + ) + backgroundTintList = ColorStateList.valueOf(TRANSPARENT) + setIconDrawable(drawable) + if (!activityContext.isTransientTaskbar) { + setPadding( + dpToPx( + (activityContext.taskbarSpecsEvaluator.taskbarIconPadding).toFloat(), + activityContext, + ) + ) + } + setForegroundTint(activityContext.getColor(R.color.all_apps_button_color)) + } + + @SuppressLint("ClickableViewAccessibility") + fun setUpCallbacks(callbacks: TaskbarViewCallbacks) { + taskbarViewCallbacks = callbacks + setOnClickListener(this::onAllAppsButtonClick) + setOnLongClickListener(this::onAllAppsButtonLongClick) + setOnTouchListener(this::onAllAppsButtonTouch) + isHapticFeedbackEnabled = + taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled(mContext) + allAppsTouchRunnable = Runnable { + taskbarViewCallbacks.triggerAllAppsButtonLongClick() + allAppsTouchTriggered = true + } + val contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[mContext] + if ( + DeviceConfigWrapper.get().customLpaaThresholds && + contextualSearchStateManager.lpnhDurationMillis.isPresent + ) { + allAppsButtonTouchDelayMs = contextualSearchStateManager.lpnhDurationMillis.get() + } + } + + @DrawableRes + private fun getAllAppsButton(isTransientTaskbar: Boolean): Int { + if (Flags.enableGsf()) { + return getAllAppsButtonForExpressiveTheme() + } + val shouldSelectTransientIcon = + isTransientTaskbar || + (enableTaskbarPinning() && + activityContext.taskbarFeatureEvaluator.supportsTransitionToTransientTaskbar) + return if (shouldSelectTransientIcon) R.drawable.ic_transient_taskbar_all_apps_search_button + else R.drawable.ic_taskbar_all_apps_search_button + } + + @DrawableRes + private fun getAllAppsButtonForExpressiveTheme(): Int { + return if (isTaskbarInMinimalState) { + R.drawable.ic_taskbar_minimal_state_all_apps_search_button_expressive_theme + } else { + R.drawable.ic_taskbar_all_apps_search_button_expressive_theme + } + } + + @DimenRes + fun getAllAppsButtonTranslationXOffset(isTransientTaskbar: Boolean): Int { + if (Flags.enableGsf()) { + return R.dimen.taskbar_all_apps_search_button_translation_x_offset_for_expressive_theme + } + return if (isTransientTaskbar) { + R.dimen.transient_taskbar_all_apps_button_translation_x_offset + } else { + R.dimen.taskbar_all_apps_search_button_translation_x_offset + } + } + + /** Taskbar minimal state is that taskbar does not host anything other than all apps button. */ + fun updateTaskbarMinimalState(isInMinimalState: Boolean) { + if (isTaskbarInMinimalState != isInMinimalState) { + isTaskbarInMinimalState = isInMinimalState + setUpIcon() + } + } + + private fun onAllAppsButtonTouch(view: View, ev: MotionEvent): Boolean { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + allAppsTouchTriggered = false + MAIN_EXECUTOR.handler.postDelayed(allAppsTouchRunnable!!, allAppsButtonTouchDelayMs) + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> cancelAllAppsButtonTouch() + } + return false + } + + private fun cancelAllAppsButtonTouch() { + MAIN_EXECUTOR.handler.removeCallbacks(allAppsTouchRunnable!!) + // ACTION_UP is first triggered, then click listener / long-click listener is triggered on + // the next frame, so we need to post twice and delay the reset. + this.post { this.post { allAppsTouchTriggered = false } } + } + + private fun onAllAppsButtonClick(view: View) { + if (!allAppsTouchTriggered) { + taskbarViewCallbacks.triggerAllAppsButtonClick(view) + } + } + + // Handle long click from Switch Access and Voice Access + private fun onAllAppsButtonLongClick(view: View): Boolean { + if (!MAIN_EXECUTOR.handler.hasCallbacks(allAppsTouchRunnable!!) && !allAppsTouchTriggered) { + taskbarViewCallbacks.triggerAllAppsButtonLongClick() + } + return true + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt index 3c4b63a1d7..35ae43cd29 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainer.kt @@ -13,15 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.launcher3.taskbar.customization -/** Enums for all feature container that taskbar supports. */ -enum class TaskbarContainer { - ALL_APPS, - DIVIDER, - APP_ICONS, - RECENTS, - NAV_BUTTONS, - BUBBLES, +import androidx.annotation.Dimension + +/** + * Interface to be implemented by all taskbar container to expose [spaceNeeded] for each container. + */ +interface TaskbarContainer { + @get:Dimension(unit = Dimension.DP) val spaceNeeded: Int } diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.kt new file mode 100644 index 0000000000..d4548f5efe --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarContainers.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.launcher3.taskbar.customization + +/** Enums for all feature container that taskbar supports. */ +enum class TaskbarContainers { + ALL_APPS, + DIVIDER, + APP_ICONS, + RECENTS, + NAV_BUTTONS, + BUBBLES, +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt new file mode 100644 index 0000000000..96dc5c8447 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt @@ -0,0 +1,82 @@ +/* + * 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.launcher3.taskbar.customization + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color.TRANSPARENT +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.core.view.setPadding +import com.android.launcher3.R +import com.android.launcher3.Utilities.dpToPx +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarViewCallbacks +import com.android.launcher3.views.ActivityContext +import com.android.launcher3.views.IconButtonView +import com.android.wm.shell.Flags + +/** Taskbar divider view container for customizable taskbar. */ +class TaskbarDividerContainer +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + IconButtonView(context, attrs), TaskbarContainer { + private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) + + override val spaceNeeded: Int + get() { + return dpToPx( + activityContext.taskbarSpecsEvaluator.taskbarIconSize.size.toFloat(), + activityContext, + ) + } + + init { + contentDescription = context.getString(R.string.taskbar_divider_a11y_title) + setUpIcon() + } + + fun setUpIcon() { + backgroundTintList = ColorStateList.valueOf(TRANSPARENT) + val drawable = getTaskbarDividerIcon() + setIconDrawable(drawable) + if (!activityContext.isTransientTaskbar) { + setPadding( + dpToPx( + activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat(), + activityContext, + ) + ) + } + } + + @SuppressLint("UseCompatLoadingForDrawables") + fun getTaskbarDividerIcon(): Drawable { + return if (Flags.enableGsf()) { + resources.getDrawable(R.drawable.taskbar_divider_button_expressive_theme) + } else { + resources.getDrawable(R.drawable.taskbar_divider_button) + } + } + + @SuppressLint("ClickableViewAccessibility") + fun setUpCallbacks(callbacks: TaskbarViewCallbacks?) { + setOnLongClickListener(callbacks?.taskbarDividerLongClickListener) + setOnTouchListener(callbacks?.taskbarDividerRightClickListener) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt index 1ec075ffaa..2de143e979 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt @@ -16,29 +16,54 @@ package com.android.launcher3.taskbar.customization +import com.android.launcher3.Flags.enableRecentsInTaskbar import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning import com.android.launcher3.taskbar.TaskbarActivityContext -import com.android.launcher3.taskbar.TaskbarControllers -import com.android.launcher3.taskbar.TaskbarRecentAppsController -import com.android.launcher3.util.DisplayController /** Evaluates all the features taskbar can have. */ -class TaskbarFeatureEvaluator( - private val taskbarActivityContext: TaskbarActivityContext, - private val taskbarControllers: TaskbarControllers, -) { - +class TaskbarFeatureEvaluator +private constructor(private val taskbarActivityContext: TaskbarActivityContext) { val hasAllApps = true val hasAppIcons = true val hasBubbles = false val hasNavButtons = taskbarActivityContext.isThreeButtonNav - val hasRecents: Boolean - get() = taskbarControllers.taskbarRecentAppsController.isEnabled + val isRecentsEnabled: Boolean + get() = enableRecentsInTaskbar() val hasDivider: Boolean - get() = enableTaskbarPinning() || hasRecents + get() = enableTaskbarPinning() || isRecentsEnabled val isTransient: Boolean - get() = DisplayController.isTransientTaskbar(taskbarActivityContext) + get() = taskbarActivityContext.isTransientTaskbar + + val isLandscape: Boolean + get() = taskbarActivityContext.deviceProfile.deviceProperties.isLandscape + + val isTnMinimalState: Boolean + get() = taskbarActivityContext.isTaskbarInMinimalState + + val supportsPinningPopup: Boolean + get() = !hasNavButtons + + val supportsTransitionToTransientTaskbar: Boolean + get() = !hasNavButtons && !taskbarActivityContext.showDesktopTaskbarForFreeformDisplay() + + fun onDestroy() { + taskbarFeatureEvaluator = null + } + + companion object { + @Volatile private var taskbarFeatureEvaluator: TaskbarFeatureEvaluator? = null + + @JvmStatic + fun getInstance(taskbarActivityContext: TaskbarActivityContext): TaskbarFeatureEvaluator { + synchronized(this) { + if (taskbarFeatureEvaluator == null) { + taskbarFeatureEvaluator = TaskbarFeatureEvaluator(taskbarActivityContext) + } + return taskbarFeatureEvaluator!! + } + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt index 4cd895de4d..e55cb1f50d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarIconSpecs.kt @@ -19,23 +19,35 @@ package com.android.launcher3.taskbar.customization /** Taskbar Icon Specs */ object TaskbarIconSpecs { - val iconSize40dp = TaskbarIconSize(40) - val iconSize44dp = TaskbarIconSize(44) - val iconSize48dp = TaskbarIconSize(48) - val iconSize52dp = TaskbarIconSize(52) + // Mapping of visual icon size to icon specs value http://b/235886078 + val iconSize40dp = TaskbarIconSize(44) + val iconSize44dp = TaskbarIconSize(48) + val iconSize48dp = TaskbarIconSize(52) + val iconSize52dp = TaskbarIconSize(57) val transientTaskbarIconSizes = arrayOf(iconSize44dp, iconSize48dp, iconSize52dp) val defaultPersistentIconSize = iconSize40dp val defaultTransientIconSize = iconSize44dp - // defined as row, columns + val minimumIconSize = iconSize40dp + + val defaultPersistentIconMargin = TaskbarIconMarginSize(6) + val defaultTransientIconMargin = TaskbarIconMarginSize(12) + + val minimumTaskbarIconTouchSize = TaskbarIconSize(48) + + val transientOrPinnedTaskbarIconPaddingSize = iconSize52dp + val transientTaskbarIconSizeByGridSize = mapOf( - Pair(6, 5) to iconSize52dp, - Pair(4, 5) to iconSize48dp, - Pair(5, 4) to iconSize48dp, - Pair(4, 4) to iconSize48dp, - Pair(5, 6) to iconSize44dp, + TransientTaskbarIconSizeKey(6, 5, false) to iconSize52dp, + TransientTaskbarIconSizeKey(6, 5, true) to iconSize52dp, + TransientTaskbarIconSizeKey(4, 4, false) to iconSize48dp, + TransientTaskbarIconSizeKey(4, 4, true) to iconSize52dp, + TransientTaskbarIconSizeKey(4, 5, false) to iconSize48dp, + TransientTaskbarIconSizeKey(4, 5, true) to iconSize48dp, + TransientTaskbarIconSizeKey(5, 5, false) to iconSize44dp, + TransientTaskbarIconSizeKey(5, 5, true) to iconSize44dp, ) } diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt index 02e5947b32..13c878ee6f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt @@ -16,13 +16,43 @@ package com.android.launcher3.taskbar.customization -/** Evaluates the taskbar specs based on the taskbar grid size and the taskbar icon size. */ -class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator) { +import com.android.launcher3.taskbar.TaskbarActivityContext - fun getIconSizeByGrid(row: Int, column: Int): TaskbarIconSize { +/** Evaluates the taskbar specs based on the taskbar grid size and the taskbar icon size. */ +class TaskbarSpecsEvaluator( + private val taskbarActivityContext: TaskbarActivityContext, + private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator, + numRows: Int = taskbarActivityContext.deviceProfile.inv.numRows, + numColumns: Int = taskbarActivityContext.deviceProfile.inv.numColumns, +) { + var taskbarIconSize: TaskbarIconSize = getIconSizeByGrid(numColumns, numRows) + val numShownHotseatIcons + get() = taskbarActivityContext.deviceProfile.numShownHotseatIcons + + // TODO(b/341146605) : initialize it to taskbar container in later cl. + private var taskbarContainer: List = emptyList() + + val taskbarIconPadding: Int = + if ( + TaskbarIconSpecs.transientOrPinnedTaskbarIconPaddingSize.size > taskbarIconSize.size && + taskbarFeatureEvaluator.supportsTransitionToTransientTaskbar + ) { + (TaskbarIconSpecs.iconSize52dp.size - taskbarIconSize.size) / 2 + } else { + 0 + } + + val taskbarIconMargin: TaskbarIconMarginSize = + if (taskbarFeatureEvaluator.isTransient) { + TaskbarIconSpecs.defaultTransientIconMargin + } else { + TaskbarIconSpecs.defaultPersistentIconMargin + } + + fun getIconSizeByGrid(columns: Int, rows: Int): TaskbarIconSize { return if (taskbarFeatureEvaluator.isTransient) { TaskbarIconSpecs.transientTaskbarIconSizeByGridSize.getOrDefault( - Pair(row, column), + TransientTaskbarIconSizeKey(columns, rows, taskbarFeatureEvaluator.isLandscape), TaskbarIconSpecs.defaultTransientIconSize, ) } else { @@ -36,8 +66,11 @@ class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureE val currentIconSizeIndex = TaskbarIconSpecs.transientTaskbarIconSizes.indexOf(iconSize) // return the current icon size if supplied icon size is unknown or we have reached the // min icon size. - return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) iconSize - else TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1] + return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) { + iconSize + } else { + TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1] + } } fun getIconSizeStepUp(iconSize: TaskbarIconSize): TaskbarIconSize { @@ -52,9 +85,28 @@ class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureE ) { iconSize } else { - TaskbarIconSpecs.transientTaskbarIconSizes.get(currentIconSizeIndex + 1) + TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex + 1] } } + + // TODO(jagrutdesai) : Call this in init once the containers are ready. + private fun calculateTaskbarIconSize() { + while ( + taskbarIconSize != TaskbarIconSpecs.minimumIconSize && + taskbarActivityContext.transientTaskbarBounds.width() < + calculateSpaceNeeded(taskbarContainer) + ) { + taskbarIconSize = getIconSizeStepDown(taskbarIconSize) + } + } + + private fun calculateSpaceNeeded(containers: List): Int { + return containers.sumOf { it.spaceNeeded } + } } data class TaskbarIconSize(val size: Int) + +data class TransientTaskbarIconSizeKey(val columns: Int, val rows: Int, val isLandscape: Boolean) + +data class TaskbarIconMarginSize(val size: Int) diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/ActionPerformers.kt b/quickstep/src/com/android/launcher3/taskbar/growth/ActionPerformers.kt new file mode 100644 index 0000000000..17c950993d --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/ActionPerformers.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar.growth + +import android.content.Context +import android.content.Intent +import android.net.Uri + +object ActionPerformers { + fun interface DismissCallback { + fun invoke() + } + + fun performActions(actions: List, context: Context, dismissCallback: DismissCallback) { + for (action in actions) { + performAction(action, context, dismissCallback) + } + } + + private fun performAction(action: Action, context: Context, dismissCallback: DismissCallback) { + when (action) { + is Action.Dismiss -> { + // TODO: b/396239267 - Handle marking the campaign dismissed with dismissal + // retention. + dismissCallback.invoke() + } + is Action.OpenUrl -> openUrl(action.url, context) + // Handle other actions + } + } + + fun openUrl(url: String, context: Context) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/GrowthConstants.java b/quickstep/src/com/android/launcher3/taskbar/growth/GrowthConstants.java new file mode 100644 index 0000000000..7d760fc9bb --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/GrowthConstants.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar.growth; + +/** + * Constants for registering Growth framework. + */ +public final class GrowthConstants { + /** + * For Taskbar broadcast intent filter. + */ + public static final String BROADCAST_SHOW_NUDGE = + "com.android.launcher3.growth.BROADCAST_SHOW_NUDGE"; + + /** + * For filtering package of broadcast intent received. + */ + public static final String GROWTH_NUDGE_PERMISSION = + "com.android.growth.permission.GROWTH_NUDGE_PERMISSION" + + " android:protectionLevel=\"signature|preinstalled\""; + + private GrowthConstants() {} +} diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/NudgeController.kt b/quickstep/src/com/android/launcher3/taskbar/growth/NudgeController.kt new file mode 100644 index 0000000000..5c5990eb24 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/NudgeController.kt @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar.growth + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.View.GONE +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.MarginLayoutParams +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.LayoutRes +import androidx.core.view.updateLayoutParams +import com.airbnb.lottie.LottieAnimationView +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_GROWTH_NUDGE_OPEN +import com.android.launcher3.taskbar.TaskbarControllers +import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController +import com.android.launcher3.views.ActivityContext +import com.android.quickstep.util.LottieAnimationColorUtils +import java.io.PrintWriter + +/** + * Controls nudge lifecycles. + * + * TODO: b/413718172 - Refactor to reduce code duplication with [TaskbarEduTooltipController]. + */ +class NudgeController(context: Context) : LoggableTaskbarController { + + protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context) + + private val isNudgeEnabled: Boolean + get() { + return !Utilities.isRunningInTestHarness() && + !activityContext.isPhoneMode && + !activityContext.isTinyTaskbar + } + + val isNudgeOpen: Boolean + get() = nudgeView?.isOpen == true + + private lateinit var controllers: TaskbarControllers + + private var nudgeView: NudgeView? = null + + fun init(controllers: TaskbarControllers) { + this.controllers = controllers + } + + fun maybeShow(model: NudgePayload) { + if (!isNudgeEnabled || !activityContext.isTransientTaskbar) { + return + } + + inflateNudgeContent(R.layout.growth_nudge) + nudgeView?.run { + allowTouchDismissal = false + + fun updateButton(button: Button, buttonPayload: ButtonPayload?) { + if (buttonPayload != null) { + button.apply { + text = buttonPayload.label + setOnClickListener { + ActionPerformers.performActions( + /*actions=*/ buttonPayload.actions, + /*context=*/ activityContext, + /*dismissCallback=*/ ::hide, + ) + } + } + } else { + button.visibility = GONE + } + } + + fun updateImage(image: Image?) { + val imageView = requireViewById(R.id.image_view) + when (image) { + is Image.ResourceId -> { + imageView.setImageDrawable(context.getDrawable(image.resId)) + } + null -> imageView.visibility = GONE + } + } + + fun updateContent() { + // Update content. + val title = requireViewById(R.id.title) + title.text = model.titleText + val body = requireViewById(R.id.body) + body.text = model.bodyText + updateButton(requireViewById(R.id.primary_button), model.primaryButton) + updateButton(requireViewById(R.id.secondary_button), model.secondaryButton) + updateImage(model.image) + } + + fun updateLayout() { + content.updateLayoutParams { width = MATCH_PARENT } + val sideSpacing = + resources.getDimensionPixelSize(R.dimen.nudge_default_position_side_spacing) + updateLayoutParams { + if (Utilities.isRtl(context.getResources())) { + rightMargin = sideSpacing + } else { + leftMargin = sideSpacing + } + width = resources.getDimensionPixelSize(R.dimen.nudge_width) + } + } + + updateContent() + updateLayout() + show() + } + } + + /** Closes the current [nudgeView]. */ + fun hide() { + nudgeView?.close(true) + } + + /** Initializes [nudgeView] with content from [contentResId]. */ + private fun inflateNudgeContent(@LayoutRes contentResId: Int) { + val overlayContext = controllers.taskbarOverlayController.requestWindow() + val nudgeView = + overlayContext.layoutInflater.inflate( + R.layout.taskbar_nudge_container, + overlayContext.dragLayer, + false, + ) as NudgeView + + controllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_GROWTH_NUDGE_OPEN, + true, + ) + + nudgeView.onCloseCallback = { + this.nudgeView = null + controllers.taskbarAutohideSuspendController.updateFlag( + FLAG_AUTOHIDE_SUSPEND_GROWTH_NUDGE_OPEN, + false, + ) + controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true) + } + nudgeView.accessibilityDelegate = createAccessibilityDelegate() + + overlayContext.layoutInflater.inflate(contentResId, nudgeView.content, true) + this.nudgeView = nudgeView + } + + private fun createAccessibilityDelegate() = + object : View.AccessibilityDelegate() { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle?, + ): Boolean { + if (action == R.id.close) { + hide() + return true + } + return super.performAccessibilityAction(host, action, args) + } + + override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) { + super.onPopulateAccessibilityEvent(host, event) + if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + event.text.add(host.context?.getText(R.string.nudge_a11y_title)) + } + } + + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo, + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction( + AccessibilityNodeInfo.AccessibilityAction( + R.id.close, + host.context?.getText(R.string.nudge_a11y_close), + ) + ) + } + } + + override fun dumpLogs(prefix: String?, pw: PrintWriter?) { + pw?.println(prefix + "NudgeController:") + pw?.println("$prefix\tisNudgeEnabled=$isNudgeEnabled") + pw?.println("$prefix\tisOpen=$isNudgeOpen") + } +} + +/** + * Maps colors in the dark-themed Lottie assets to their light-themed equivalents. + * + * For instance, `".blue100" to R.color.lottie_blue400` means objects that are material blue100 in + * dark theme should be changed to material blue400 in light theme. + */ +private val DARK_TO_LIGHT_COLORS = + mapOf( + ".blue100" to R.color.lottie_blue400, + ".blue400" to R.color.lottie_blue600, + ".green100" to R.color.lottie_green400, + ".green400" to R.color.lottie_green600, + ".grey300" to R.color.lottie_grey600, + ".grey400" to R.color.lottie_grey700, + ".grey800" to R.color.lottie_grey200, + ".red400" to R.color.lottie_red600, + ".yellow100" to R.color.lottie_yellow400, + ".yellow400" to R.color.lottie_yellow600, + ) + +private fun LottieAnimationView.supportLightTheme() { + if (Utilities.isDarkTheme(context)) { + return + } + + LottieAnimationColorUtils.updateToColorResources(this, DARK_TO_LIGHT_COLORS, context.theme) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/NudgePayload.kt b/quickstep/src/com/android/launcher3/taskbar/growth/NudgePayload.kt new file mode 100644 index 0000000000..7c167b959d --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/NudgePayload.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar.growth + +sealed interface Action { + data class Dismiss( + val markAsDismissed: Boolean = true, + val dismissRetentionInDays: Int? = null, + ) : Action + + data class OpenUrl(val url: String) : Action +} + +sealed class Image { + data class ResourceId(val resId: Int) : Image() +} + +data class ButtonPayload(val label: String, val actions: List) + +data class NudgePayload( + val titleText: String, + val bodyText: String, + val image: Image?, + val primaryButton: ButtonPayload?, + val secondaryButton: ButtonPayload?, + + // TODO: b/396223717 - add anchoring information. +) diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/NudgeView.kt b/quickstep/src/com/android/launcher3/taskbar/growth/NudgeView.kt new file mode 100644 index 0000000000..5a832b1423 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/NudgeView.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.taskbar.growth + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.ViewGroup +import android.view.animation.Interpolator +import android.window.OnBackInvokedDispatcher +import androidx.core.view.updateLayoutParams +import com.android.app.animation.Interpolators +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.R +import com.android.launcher3.anim.AnimatorListeners +import com.android.launcher3.views.ActivityContext + +private const val ENTER_DURATION_MS = 300L +private const val EXIT_DURATION_MS = 150L + +/** Floating nudge. */ +class NudgeView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + AbstractFloatingView(context, attrs, defStyleAttr) { + + private val activityContext: ActivityContext = ActivityContext.lookupContext(context) + + private val enterYDelta = resources.getDimension(R.dimen.nudge_enter_y_delta) + private val exitYDelta = resources.getDimension(R.dimen.nudge_exit_y_delta) + + /** Container where the nudge's body should be inflated. */ + lateinit var content: ViewGroup + private set + + /** Callback invoked when the nudge is being closed. */ + var onCloseCallback: () -> Unit = {} + private var openCloseAnimator: AnimatorSet? = null + /** Used to set whether users can tap outside the current nudge window to dismiss it */ + var allowTouchDismissal = true + + /** Animates the nudge into view. */ + fun show() { + if (isOpen) { + return + } + + mIsOpen = true + activityContext.dragLayer.addView(this) + + // Make sure we have enough height to display all of the content, which can be an issue on + // large text and display scaling configurations. If we run out of height, remove the width + // constraint to reduce the number of lines of text and hopefully free up some height. + activityContext.dragLayer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + if ( + measuredHeight + activityContext.deviceProfile.taskbarProfile.height >= + activityContext.deviceProfile.deviceProperties.availableHeightPx + ) { + updateLayoutParams { width = LayoutParams.MATCH_PARENT } + } + + openCloseAnimator = createOpenCloseAnimator(isOpening = true).apply { start() } + } + + override fun onFinishInflate() { + super.onFinishInflate() + + content = requireViewById(R.id.content) + } + + override fun handleClose(animate: Boolean) { + if (!isOpen) { + return + } + + onCloseCallback() + if (!animate) { + return closeComplete() + } + + openCloseAnimator?.cancel() + openCloseAnimator = createOpenCloseAnimator(isOpening = false) + openCloseAnimator?.apply { + addListener(AnimatorListeners.forEndCallback(this@NudgeView::closeComplete)) + start() + } + } + + override fun isOfType(type: Int): Boolean = type and TYPE_NUDGE != 0 + + override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { + if ( + ev?.action == MotionEvent.ACTION_DOWN && + !activityContext.dragLayer.isEventOverView(this, ev) && + allowTouchDismissal + ) { + close(true) + } + return false + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + findOnBackInvokedDispatcher() + ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this) + // TODO: b/396507770 - investigate and coordinate with letterbox flow. + } + + private fun closeComplete() { + openCloseAnimator?.cancel() + openCloseAnimator = null + mIsOpen = false + activityContext.dragLayer.removeView(this) + } + + private fun createOpenCloseAnimator(isOpening: Boolean): AnimatorSet { + val duration: Long + val alphaValues: FloatArray + val translateYValues: FloatArray + val fadeInterpolator: Interpolator + val translateYInterpolator: Interpolator + + if (isOpening) { + duration = ENTER_DURATION_MS + alphaValues = floatArrayOf(0f, 1f) + translateYValues = floatArrayOf(enterYDelta, 0f) + fadeInterpolator = Interpolators.STANDARD + translateYInterpolator = Interpolators.EMPHASIZED_DECELERATE + } else { + duration = EXIT_DURATION_MS + alphaValues = floatArrayOf(1f, 0f) + translateYValues = floatArrayOf(0f, exitYDelta) + fadeInterpolator = Interpolators.EMPHASIZED_ACCELERATE + translateYInterpolator = Interpolators.EMPHASIZED_ACCELERATE + } + + val fade = + ValueAnimator.ofFloat(*alphaValues).apply { + interpolator = fadeInterpolator + addUpdateListener { + val alpha = it.animatedValue as Float + content.alpha = alpha + } + } + + val translateY = + ValueAnimator.ofFloat(*translateYValues).apply { + interpolator = translateYInterpolator + addUpdateListener { + val translationY = it.animatedValue as Float + content.translationY = translationY + } + } + + return AnimatorSet().apply { + this.duration = duration + playTogether(fade, translateY) + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/README b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/README new file mode 100644 index 0000000000..36c583ba7d --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/README @@ -0,0 +1,3 @@ +This directory contains growth framework nudge proto definition for communication with the Growth App. + +Keep in sync with the proto in //depot/google3/java/com/google/android/desktop/growth/proto \ No newline at end of file diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/button.proto b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/button.proto new file mode 100644 index 0000000000..47f6b53e91 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/button.proto @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package com.google.android.desktop.growth.proto; + +import "google/protobuf/duration.proto"; + +option java_package = "com.google.android.desktop.growth.proto"; +option java_outer_classname = "Nudge"; +option java_multiple_files = true; + +message DismissParam { + // Optional. Whether to report the action as dismissed. + optional bool mark_as_dismissed = 1 [default = true]; + + // Optional. The duration specifies how long to wait before showing the nudge + // again after it was auto dismissed without user interaction. Will be ignored + // if mark_as_dismissed is false. + optional google.protobuf.Duration duration = 2; +} + +message OpenUrlParam { + // Required. The URL to open. + required string url = 1; +} + +// Action to be performed when the button is clicked. +message Action { + oneof param { + DismissParam dismiss_param = 1; + OpenUrlParam open_url_param = 2; + } +} + +// Payload for a nudge button. +message ButtonPayload { + // Required. Contains the label of the button. + required string label = 1; + + // Optional. Contains the actions of the button. If not specified, the default + // action is to dismiss the nudge. + repeated Action actions = 2; +} diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/image.proto b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/image.proto new file mode 100644 index 0000000000..42d5618c4e --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/image.proto @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package com.google.android.desktop.growth.proto; + +option java_package = "com.google.android.desktop.growth.proto"; +option java_outer_classname = "Nudge"; +option java_multiple_files = true; + +// Image object. This can be used to represent an image using the URL. +message ImagePayload { + oneof image { + // Contains the resource URL of the image. + string url = 1; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/payload.proto b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/payload.proto new file mode 100644 index 0000000000..c5d39021af --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/growth/proto/nudge/payload.proto @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package com.google.android.desktop.growth.proto; + +option java_package = "com.google.android.desktop.growth.proto"; +option java_outer_classname = "Nudge"; +option java_multiple_files = true; + +// Nudge payload used for passing data from Growth Framework to the UI +// Component. +message NudgePayload { + // Required. Contains the title text of the nudge. + string title_text = 1; + + // Required. Contains the body text of the nudge. + string body_text = 2; + + // Optional. Contains the image of the nudge. + ImagePayload image = 3; + + // Optional. The primary button of the nudge. + ButtonPayload primary_button = 4; + + // Optional. The secondary button of the nudge. Typically for the + // "Dismiss" button. + ButtonPayload secondary_button = 5; +} \ No newline at end of file diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt index e487f9fd40..918d5cf71e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt @@ -27,7 +27,6 @@ import android.widget.Space import com.android.launcher3.DeviceProfile import com.android.launcher3.R import com.android.launcher3.Utilities -import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter /** @@ -48,7 +47,7 @@ abstract class AbstractNavButtonLayoutter( protected val startContextualContainer: ViewGroup, protected val imeSwitcher: ImageView?, protected val a11yButton: ImageView?, - protected val space: Space? + protected val space: Space?, ) : NavButtonLayoutter { protected val homeButton: ImageView? = navButtonContainer.findViewById(R.id.home) protected val recentsButton: ImageView? = navButtonContainer.findViewById(R.id.recent_apps) @@ -69,26 +68,34 @@ abstract class AbstractNavButtonLayoutter( val params = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT + ViewGroup.LayoutParams.MATCH_PARENT, ) params.gravity = Gravity.CENTER return params } + /** + * Adjusts the layout parameters of the nav bar container for setup in phone mode. + * + * @param nearestTouchFrameLayoutParams The layout parameters of the navButtonsView, which is + * the ViewGroup that contains start, end, nav button ViewGroups + * @param deviceProfile The device profile containing information about the device's + * configuration. + */ fun adjustForSetupInPhoneMode( - navButtonsLayoutParams: FrameLayout.LayoutParams, - navButtonsViewLayoutParams: FrameLayout.LayoutParams, - deviceProfile: DeviceProfile + nearestTouchFrameLayoutParams: FrameLayout.LayoutParams, + deviceProfile: DeviceProfile, ) { val phoneOrPortraitSetupMargin = resources.getDimensionPixelSize(R.dimen.taskbar_contextual_button_suw_margin) - navButtonsLayoutParams.marginStart = phoneOrPortraitSetupMargin - navButtonsLayoutParams.bottomMargin = - if (!deviceProfile.isLandscape) 0 + nearestTouchFrameLayoutParams.marginStart = phoneOrPortraitSetupMargin + nearestTouchFrameLayoutParams.bottomMargin = + if (!deviceProfile.deviceProperties.isLandscape) 0 else phoneOrPortraitSetupMargin - - resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) / 2 - navButtonsViewLayoutParams.height = + resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) / 2 + + nearestTouchFrameLayoutParams.height = resources.getDimensionPixelSize(R.dimen.taskbar_contextual_button_suw_height) } @@ -97,7 +104,7 @@ abstract class AbstractNavButtonLayoutter( buttonSize: Int, barAxisMarginStart: Int, barAxisMarginEnd: Int, - gravity: Int + gravity: Int, ) { val contextualContainerParams = FrameLayout.LayoutParams(buttonSize, ViewGroup.LayoutParams.MATCH_PARENT) diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt index 2497fbb98e..17e63e8bd3 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt @@ -66,7 +66,7 @@ class NavButtonLayoutFactory { isInSetup: Boolean, isThreeButtonNav: Boolean, phoneMode: Boolean, - @Rotation surfaceRotation: Int + @Rotation surfaceRotation: Int, ): NavButtonLayoutter { val navButtonContainer = navButtonsView.requireViewById(ID_END_NAV_BUTTONS) @@ -77,8 +77,20 @@ class NavButtonLayoutFactory { val isPhoneNavMode = phoneMode && isThreeButtonNav val isPhoneGestureMode = phoneMode && !isThreeButtonNav return when { + isInSetup -> { + SetupNavLayoutter( + resources, + navButtonsView, + navButtonContainer, + endContextualContainer, + startContextualContainer, + imeSwitcher, + a11yButton, + space, + ) + } isPhoneNavMode -> { - if (!deviceProfile.isLandscape) { + if (!deviceProfile.deviceProperties.isLandscape) { navButtonsView.setIsVertical(false) PhonePortraitNavLayoutter( resources, @@ -87,7 +99,7 @@ class NavButtonLayoutFactory { startContextualContainer, imeSwitcher, a11yButton, - space + space, ) } else if (surfaceRotation == ROTATION_90) { navButtonsView.setIsVertical(true) @@ -98,7 +110,7 @@ class NavButtonLayoutFactory { startContextualContainer, imeSwitcher, a11yButton, - space + space, ) } else { navButtonsView.setIsVertical(true) @@ -109,36 +121,23 @@ class NavButtonLayoutFactory { startContextualContainer, imeSwitcher, a11yButton, - space + space, ) } } isPhoneGestureMode -> { PhoneGestureLayoutter( resources, - navButtonsView, navButtonContainer, endContextualContainer, startContextualContainer, imeSwitcher, a11yButton, - space + space, ) } deviceProfile.isTaskbarPresent -> { return when { - isInSetup -> { - SetupNavLayoutter( - resources, - navButtonsView, - navButtonContainer, - endContextualContainer, - startContextualContainer, - imeSwitcher, - a11yButton, - space - ) - } isKidsMode -> { KidsNavLayoutter( resources, @@ -147,7 +146,7 @@ class NavButtonLayoutFactory { startContextualContainer, imeSwitcher, a11yButton, - space + space, ) } else -> @@ -158,7 +157,7 @@ class NavButtonLayoutFactory { startContextualContainer, imeSwitcher, a11yButton, - space + space, ) } } diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java b/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java index bbf08bf903..844f1af288 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NearestTouchFrame.java @@ -194,6 +194,7 @@ public class NearestTouchFrame extends FrameLayout { event.offsetLocation(mTouchingChild.getWidth() / 2 - x, mTouchingChild.getHeight() / 2 - y); return mTouchingChild.getVisibility() == VISIBLE + && mTouchingChild.isAttachedToWindow() && mTouchingChild.dispatchTouchEvent(event); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt index 390ec342e1..e0f2a22b2c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt @@ -17,25 +17,21 @@ package com.android.launcher3.taskbar.navbutton import android.content.res.Resources -import android.view.Gravity import android.view.ViewGroup -import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.Space -import com.android.launcher3.DeviceProfile import com.android.launcher3.taskbar.TaskbarActivityContext /** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */ class PhoneGestureLayoutter( resources: Resources, - navButtonsView: NearestTouchFrame, navBarContainer: LinearLayout, endContextualContainer: ViewGroup, startContextualContainer: ViewGroup, imeSwitcher: ImageView?, a11yButton: ImageView?, - space: Space? + space: Space?, ) : AbstractNavButtonLayoutter( resources, @@ -44,33 +40,10 @@ class PhoneGestureLayoutter( startContextualContainer, imeSwitcher, a11yButton, - space + space, ) { - private val mNavButtonsView = navButtonsView override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) { - // TODO: look into if we should use SetupNavLayoutter instead. - if (!context.isUserSetupComplete) { - // Since setup wizard only has back button enabled, it looks strange to be - // end-aligned, so start-align instead. - val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams - val navButtonsViewLayoutParams = - mNavButtonsView.layoutParams as FrameLayout.LayoutParams - val deviceProfile: DeviceProfile = context.deviceProfile - - navButtonsLayoutParams.marginEnd = 0 - navButtonsLayoutParams.gravity = Gravity.START - context.setTaskbarWindowSize(context.setupWindowSize) - - adjustForSetupInPhoneMode( - navButtonsLayoutParams, - navButtonsViewLayoutParams, - deviceProfile - ) - mNavButtonsView.layoutParams = navButtonsViewLayoutParams - navButtonContainer.layoutParams = navButtonsLayoutParams - } - endContextualContainer.removeAllViews() startContextualContainer.removeAllViews() } diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt index 9f7f07e773..00ba40c7f9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt @@ -25,6 +25,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.Space import com.android.launcher3.R +import com.android.launcher3.Utilities import com.android.launcher3.taskbar.TaskbarActivityContext open class PhoneLandscapeNavLayoutter( @@ -34,7 +35,7 @@ open class PhoneLandscapeNavLayoutter( startContextualContainer: ViewGroup, imeSwitcher: ImageView?, a11yButton: ImageView?, - space: Space? + space: Space?, ) : AbstractNavButtonLayoutter( resources, @@ -43,11 +44,11 @@ open class PhoneLandscapeNavLayoutter( startContextualContainer, imeSwitcher, a11yButton, - space + space, ) { override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) { - val totalHeight = context.deviceProfile.heightPx + val totalHeight = context.deviceProfile.deviceProperties.heightPx val homeButtonHeight = resources.getDimensionPixelSize(R.dimen.taskbar_phone_home_button_size) val roundedCornerContentMargin = @@ -112,9 +113,15 @@ open class PhoneLandscapeNavLayoutter( open fun addThreeButtons() { // Swap recents and back button - navButtonContainer.addView(recentsButton) - navButtonContainer.addView(homeButton) - navButtonContainer.addView(backButton) + if (Utilities.isRtl(resources)) { + navButtonContainer.addView(backButton) + navButtonContainer.addView(homeButton) + navButtonContainer.addView(recentsButton) + } else { + navButtonContainer.addView(recentsButton) + navButtonContainer.addView(homeButton) + navButtonContainer.addView(backButton) + } } open fun repositionContextualButtons(buttonSize: Int) { @@ -129,14 +136,14 @@ open class PhoneLandscapeNavLayoutter( buttonSize, roundedCornerContentMargin + contentPadding, 0, - Gravity.TOP + Gravity.TOP, ) repositionContextualContainer( endContextualContainer, buttonSize, 0, roundedCornerContentMargin + contentPadding, - Gravity.BOTTOM + Gravity.BOTTOM, ) if (imeSwitcher != null) { @@ -155,7 +162,7 @@ open class PhoneLandscapeNavLayoutter( buttonSize: Int, barAxisMarginTop: Int, barAxisMarginBottom: Int, - gravity: Int + gravity: Int, ) { val contextualContainerParams = FrameLayout.LayoutParams(MATCH_PARENT, buttonSize) contextualContainerParams.apply { diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt index 5b24ebfe1d..833a309e83 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt @@ -34,7 +34,7 @@ class PhonePortraitNavLayoutter( startContextualContainer: ViewGroup, imeSwitcher: ImageView?, a11yButton: ImageView?, - space: Space? + space: Space?, ) : AbstractNavButtonLayoutter( resources, @@ -43,11 +43,11 @@ class PhonePortraitNavLayoutter( startContextualContainer, imeSwitcher, a11yButton, - space + space, ) { override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) { - val totalWidth = context.deviceProfile.widthPx + val totalWidth = context.deviceProfile.deviceProperties.widthPx val homeButtonWidth = resources.getDimensionPixelSize(R.dimen.taskbar_phone_home_button_size) val roundedCornerContentMargin = @@ -63,7 +63,7 @@ class PhonePortraitNavLayoutter( val navContainerParams = FrameLayout.LayoutParams( navButtonContainerWidth.toInt(), - ViewGroup.LayoutParams.MATCH_PARENT + ViewGroup.LayoutParams.MATCH_PARENT, ) navContainerParams.apply { topMargin = 0 @@ -120,14 +120,14 @@ class PhonePortraitNavLayoutter( contextualButtonWidth.toInt(), roundedCornerContentMargin + contentPadding, 0, - Gravity.START + Gravity.START, ) repositionContextualContainer( endContextualContainer, contextualButtonWidth.toInt(), 0, roundedCornerContentMargin + contentPadding, - Gravity.END + Gravity.END, ) startContextualContainer.addView(space, MATCH_PARENT, MATCH_PARENT) diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt index f0b47f4caa..cf6f89857a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt @@ -24,6 +24,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.Space import com.android.launcher3.R +import com.android.launcher3.Utilities class PhoneSeascapeNavLayoutter( resources: Resources, @@ -32,7 +33,7 @@ class PhoneSeascapeNavLayoutter( startContextualContainer: ViewGroup, imeSwitcher: ImageView?, a11yButton: ImageView?, - space: Space? + space: Space?, ) : PhoneLandscapeNavLayoutter( resources, @@ -41,14 +42,20 @@ class PhoneSeascapeNavLayoutter( startContextualContainer, imeSwitcher, a11yButton, - space + space, ) { override fun addThreeButtons() { // Flip ordering of back and recents buttons - navButtonContainer.addView(backButton) - navButtonContainer.addView(homeButton) - navButtonContainer.addView(recentsButton) + if (Utilities.isRtl(resources)) { + navButtonContainer.addView(recentsButton) + navButtonContainer.addView(homeButton) + navButtonContainer.addView(backButton) + } else { + navButtonContainer.addView(backButton) + navButtonContainer.addView(homeButton) + navButtonContainer.addView(recentsButton) + } } override fun repositionContextualButtons(buttonSize: Int) { @@ -63,14 +70,14 @@ class PhoneSeascapeNavLayoutter( buttonSize, roundedCornerContentMargin + contentPadding, 0, - Gravity.TOP + Gravity.TOP, ) repositionContextualContainer( endContextualContainer, buttonSize, 0, roundedCornerContentMargin + contentPadding, - Gravity.BOTTOM + Gravity.BOTTOM, ) startContextualContainer.addView(space, MATCH_PARENT, MATCH_PARENT) diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt index 22a36301b3..24998d4178 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt @@ -17,6 +17,7 @@ package com.android.launcher3.taskbar.navbutton import android.content.res.Resources +import android.os.SystemProperties import android.view.Gravity import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.WRAP_CONTENT @@ -28,18 +29,21 @@ import com.android.launcher3.DeviceProfile import com.android.launcher3.R import com.android.launcher3.taskbar.TaskbarActivityContext +const val SUW_THEME_SYSTEM_PROPERTY = "setupwizard.theme" +const val GLIF_EXPRESSIVE_THEME = "glif_expressive" +const val GLIF_EXPRESSIVE_LIGHT_THEME = "glif_expressive_light" const val SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95 const val SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05 class SetupNavLayoutter( resources: Resources, - navButtonsView: NearestTouchFrame, + nearestTouchFrame: NearestTouchFrame, navButtonContainer: LinearLayout, endContextualContainer: ViewGroup, startContextualContainer: ViewGroup, imeSwitcher: ImageView?, a11yButton: ImageView?, - space: Space? + space: Space?, ) : AbstractNavButtonLayoutter( resources, @@ -48,42 +52,46 @@ class SetupNavLayoutter( startContextualContainer, imeSwitcher, a11yButton, - space + space, ) { - private val mNavButtonsView = navButtonsView + // mNearestTouchFrame is a ViewGroup that contains start, end, nav button ViewGroups + private val mNearestTouchFrame = nearestTouchFrame override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) { + val SUWTheme = SystemProperties.get(SUW_THEME_SYSTEM_PROPERTY, "") + val expressiveThemeEnabled = + SUWTheme == GLIF_EXPRESSIVE_THEME || SUWTheme == GLIF_EXPRESSIVE_LIGHT_THEME + if (expressiveThemeEnabled && !context.isSimpleViewEnabled) { + return + } // Since setup wizard only has back button enabled, it looks strange to be // end-aligned, so start-align instead. val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams - val navButtonsViewLayoutParams = mNavButtonsView.layoutParams as FrameLayout.LayoutParams + val navButtonsOverallViewGroupLayoutParams = + mNearestTouchFrame.layoutParams as FrameLayout.LayoutParams val deviceProfile: DeviceProfile = context.deviceProfile navButtonsLayoutParams.marginEnd = 0 - navButtonsLayoutParams.gravity = Gravity.START + navButtonsLayoutParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL context.setTaskbarWindowSize(context.setupWindowSize) // If SUW is on a large screen device that is landscape (or has a square aspect // ratio) the back button has to be placed accordingly if ( - deviceProfile.isTablet && deviceProfile.isLandscape || - (deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND && - deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND) + deviceProfile.deviceProperties.isTablet && deviceProfile.deviceProperties.isLandscape || + (deviceProfile.deviceProperties.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND && + deviceProfile.deviceProperties.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND) ) { navButtonsLayoutParams.marginStart = resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_start_margin) - navButtonsViewLayoutParams.bottomMargin = + navButtonsOverallViewGroupLayoutParams.bottomMargin = resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_bottom_margin) navButtonsLayoutParams.height = resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_height) } else { - adjustForSetupInPhoneMode( - navButtonsLayoutParams, - navButtonsViewLayoutParams, - deviceProfile - ) + adjustForSetupInPhoneMode(navButtonsOverallViewGroupLayoutParams, deviceProfile) } - mNavButtonsView.layoutParams = navButtonsViewLayoutParams + mNearestTouchFrame.layoutParams = navButtonsOverallViewGroupLayoutParams navButtonContainer.layoutParams = navButtonsLayoutParams endContextualContainer.removeAllViews() @@ -97,7 +105,7 @@ class SetupNavLayoutter( WRAP_CONTENT, contextualMargin, contextualMargin, - Gravity.START + Gravity.START, ) if (imeSwitcher != null) { diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt index a59e8a847b..bef432e668 100644 --- a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt +++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt @@ -35,7 +35,7 @@ class TaskbarNavLayoutter( startContextualContainer: ViewGroup, imeSwitcher: ImageView?, a11yButton: ImageView?, - space: Space? + space: Space?, ) : AbstractNavButtonLayoutter( resources, @@ -44,7 +44,7 @@ class TaskbarNavLayoutter( startContextualContainer, imeSwitcher, a11yButton, - space + space, ) { override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) { @@ -69,7 +69,7 @@ class TaskbarNavLayoutter( val navButtonParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.MATCH_PARENT + ViewGroup.LayoutParams.MATCH_PARENT, ) navButtonParams.apply { gravity = Gravity.END or Gravity.CENTER_VERTICAL @@ -101,7 +101,7 @@ class TaskbarNavLayoutter( endContextualContainer.removeAllViews() startContextualContainer.removeAllViews() - if (!context.deviceProfile.isGestureMode) { + if (!context.deviceProfile.deviceProperties.isGestureMode) { val contextualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_contextual_button_padding) repositionContextualContainer(endContextualContainer, WRAP_CONTENT, 0, 0, Gravity.END) @@ -110,7 +110,7 @@ class TaskbarNavLayoutter( WRAP_CONTENT, contextualMargin, contextualMargin, - Gravity.START + Gravity.START, ) if (imeSwitcher != null) { @@ -122,7 +122,7 @@ class TaskbarNavLayoutter( val imeSwitcherButtonParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT + ViewGroup.LayoutParams.MATCH_PARENT, ) imeSwitcherButtonParams.apply { marginStart = imeStartMargin diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java index bed23b3976..c964474912 100644 --- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java @@ -16,14 +16,15 @@ package com.android.launcher3.taskbar.overlay; import android.content.Context; +import android.graphics.Point; import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.R; -import com.android.launcher3.dot.DotInfo; -import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.taskbar.BaseTaskbarContext; import com.android.launcher3.taskbar.TaskbarActivityContext; @@ -32,6 +33,7 @@ import com.android.launcher3.taskbar.TaskbarDragController; import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsContainerView; import com.android.launcher3.taskbar.allapps.TaskbarSearchSessionController; +import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; /** @@ -57,15 +59,22 @@ public class TaskbarOverlayContext extends BaseTaskbarContext { Context windowContext, TaskbarActivityContext taskbarContext, TaskbarControllers controllers) { - super(windowContext); + super(windowContext, taskbarContext.getDisplayId(), taskbarContext.isPrimaryDisplay()); mTaskbarContext = taskbarContext; mOverlayController = controllers.taskbarOverlayController; mDragController = new TaskbarDragController(this); mDragController.init(controllers); mDragLayer = new TaskbarOverlayDragLayer(this); mStashedTaskbarHeight = controllers.taskbarStashController.getStashedHeight(); + updateBlurStyle(); mUiController = controllers.uiController; + onViewCreated(); + } + + /** Called when the controller is destroyed. */ + public void onDestroy() { + mDragController.onDestroy(); } public @Nullable TaskbarSearchSessionController getSearchSessionController() { @@ -120,8 +129,17 @@ public class TaskbarOverlayContext extends BaseTaskbarContext { } @Override - public boolean isBindingItems() { - return mTaskbarContext.isBindingItems(); + public boolean isAllAppsBackgroundBlurEnabled() { + return Flags.allAppsBlur() && mOverlayController != null + && mOverlayController.isBackgroundBlurEnabled(); + } + + /** Apply the blur or blur fallback style to the current theme. */ + private void updateBlurStyle() { + if (!Flags.allAppsBlur()) { + return; + } + getTheme().applyStyle(getAllAppsBlurStyleResId(), true); } @Override @@ -134,6 +152,7 @@ public class TaskbarOverlayContext extends BaseTaskbarContext { return mDragController::startDragOnLongClick; } + @NonNull @Override public PopupDataProvider getPopupDataProvider() { return mTaskbarContext.getPopupDataProvider(); @@ -145,8 +164,53 @@ public class TaskbarOverlayContext extends BaseTaskbarContext { } @Override - public DotInfo getDotInfoForItem(ItemInfo info) { - return mTaskbarContext.getDotInfoForItem(info); + public boolean isTransientTaskbar() { + return mTaskbarContext.isTransientTaskbar(); + } + + @Override + public boolean isPinnedTaskbar() { + return mTaskbarContext.isPinnedTaskbar(); + } + + @Override + public NavigationMode getNavigationMode() { + return mTaskbarContext.getNavigationMode(); + } + + @Override + public boolean isInDesktopMode() { + return mTaskbarContext.isInDesktopMode(); + } + + @Override + public boolean isTaskbarShowingDesktopTasks() { + return mTaskbarContext.isTaskbarShowingDesktopTasks(); + } + + @Override + public boolean showLockedTaskbarOnHome() { + return mTaskbarContext.showLockedTaskbarOnHome(); + } + + @Override + public boolean showDesktopTaskbarForFreeformDisplay() { + return mTaskbarContext.showDesktopTaskbarForFreeformDisplay(); + } + + @Override + public Point getScreenSize() { + return mTaskbarContext.getScreenSize(); + } + + @Override + public int getDisplayHeight() { + return mTaskbarContext.getDisplayHeight(); + } + + @Override + public void notifyConfigChanged() { + mTaskbarContext.notifyConfigChanged(); } @Override diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContextFactory.kt b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContextFactory.kt new file mode 100644 index 0000000000..8606df71f5 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContextFactory.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.overlay + +import android.content.Context +import com.android.launcher3.dagger.LauncherComponentProvider +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarControllers +import javax.inject.Inject + +/** Creates [TaskbarOverlayContext] instances. */ +open class TaskbarOverlayContextFactory @Inject constructor() { + + open fun create( + windowContext: Context, + activityContext: TaskbarActivityContext, + controllers: TaskbarControllers, + ): TaskbarOverlayContext = TaskbarOverlayContext(windowContext, activityContext, controllers) + + companion object { + @JvmStatic + fun newInstance(context: Context): TaskbarOverlayContextFactory { + return LauncherComponentProvider.get(context).getTaskbarOverlayContextFactory() + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java index 7eb34a51c8..4905287356 100644 --- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java @@ -15,9 +15,11 @@ */ package com.android.launcher3.taskbar.overlay; +import static android.os.Trace.TRACE_TAG_APP; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static android.window.DesktopModeFlags.ENABLE_TASKBAR_OVERFLOW; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; @@ -26,8 +28,16 @@ import static com.android.launcher3.LauncherState.ALL_APPS; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; +import android.gui.EarlyWakeupInfo; +import android.os.Binder; +import android.os.Trace; +import android.util.Log; +import android.view.AttachedSurfaceControl; +import android.view.CrossWindowBlurListeners; import android.view.Gravity; import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; @@ -35,8 +45,11 @@ import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; +import com.android.launcher3.R; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.systemui.shared.system.BlurUtils; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; @@ -51,12 +64,14 @@ import java.util.Optional; */ public final class TaskbarOverlayController { + private static final String TAG = "TaskbarOverlayController"; private static final String WINDOW_TITLE = "Taskbar Overlay"; private final TaskbarActivityContext mTaskbarContext; private final Context mWindowContext; private final TaskbarOverlayProxyView mProxyView; private final LayoutParams mLayoutParams; + private final int mMaxBlurRadius; private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override @@ -87,6 +102,12 @@ public final class TaskbarOverlayController { private DeviceProfile mLauncherDeviceProfile; private @Nullable TaskbarOverlayContext mOverlayContext; private TaskbarControllers mControllers; // Initialized in init. + // True if we have alerted surface flinger of an expensive call for blur. + private boolean mInEarlyWakeUp; + /** + * Token for early wakeup requests to SurfaceFlinger. + */ + private EarlyWakeupInfo mEarlyWakeupInfo = new EarlyWakeupInfo(); public TaskbarOverlayController( TaskbarActivityContext taskbarContext, DeviceProfile launcherDeviceProfile) { @@ -95,6 +116,10 @@ public final class TaskbarOverlayController { mProxyView = new TaskbarOverlayProxyView(); mLayoutParams = createLayoutParams(); mLauncherDeviceProfile = launcherDeviceProfile; + mMaxBlurRadius = mTaskbarContext.getResources().getDimensionPixelSize( + R.dimen.max_depth_blur_radius_enhanced); + mEarlyWakeupInfo.token = new Binder(); + mEarlyWakeupInfo.trace = TaskbarOverlayController.class.getName(); } /** Initialize the controller. */ @@ -108,7 +133,7 @@ public final class TaskbarOverlayController { */ public TaskbarOverlayContext requestWindow() { if (mOverlayContext == null) { - mOverlayContext = new TaskbarOverlayContext( + mOverlayContext = TaskbarOverlayContextFactory.newInstance(mWindowContext).create( mWindowContext, mTaskbarContext, mControllers); } @@ -149,9 +174,13 @@ public final class TaskbarOverlayController { /** Destroys the controller and any overlay window if present. */ public void onDestroy() { TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); - Optional.ofNullable(mOverlayContext) - .map(c -> c.getSystemService(WindowManager.class)) - .ifPresent(m -> m.removeViewImmediate(mOverlayContext.getDragLayer())); + Optional.ofNullable(mOverlayContext).ifPresent(c -> { + c.onDestroy(); + WindowManager wm = c.getSystemService(WindowManager.class); + if (wm != null) { + wm.removeViewImmediate(mOverlayContext.getDragLayer()); + } + }); mOverlayContext = null; } @@ -160,7 +189,7 @@ public final class TaskbarOverlayController { return mLauncherDeviceProfile; } - /** Updates {@link DeviceProfile} instance for Taskbar's overlay window. */ + /** Updates {@link deviceprofile} instance for Taskbar's overlay window. */ public void updateLauncherDeviceProfile(DeviceProfile dp) { mLauncherDeviceProfile = dp; Optional.ofNullable(mOverlayContext).ifPresent(c -> { @@ -183,7 +212,7 @@ public final class TaskbarOverlayController { private LayoutParams createLayoutParams() { LayoutParams layoutParams = new LayoutParams( TYPE_APPLICATION_OVERLAY, - LayoutParams.FLAG_SPLIT_TOUCH, + /* flags = */ 0, PixelFormat.TRANSLUCENT); layoutParams.setTitle(WINDOW_TITLE); layoutParams.gravity = Gravity.BOTTOM; @@ -195,6 +224,89 @@ public final class TaskbarOverlayController { return layoutParams; } + /** + * Sets the blur radius for the overlay window. + * + * @param radius the blur radius in pixels. This will automatically change to {@code 0} if blurs + * are unsupported on the device. + */ + public void setBackgroundBlurRadius(int radius) { + if (!Flags.allAppsBlur()) { + return; + } + if (!BlurUtils.supportsBlursOnWindows()) { + Log.d(TAG, "setBackgroundBlurRadius: not supported, setting to 0"); + radius = 0; + // intentionally falling through in case a non-0 blur was previously set. + } + if (!CrossWindowBlurListeners.getInstance().isCrossWindowBlurEnabled()) { + Log.d(TAG, "setBackgroundBlurRadius: disabled, setting to 0"); + radius = 0; + // intentionally falling through in case a non-0 blur was previously set. + } + if (mOverlayContext == null) { + Log.w(TAG, "setBackgroundBlurRadius: no overlay context"); + return; + } + TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer(); + if (dragLayer == null) { + Log.w(TAG, "setBackgroundBlurRadius: no drag layer"); + return; + } + ViewRootImpl dragLayerViewRoot = dragLayer.getViewRootImpl(); + if (dragLayerViewRoot == null) { + Log.w(TAG, "setBackgroundBlurRadius: dragLayerViewRoot is null"); + return; + } + AttachedSurfaceControl rootSurfaceControl = dragLayer.getRootSurfaceControl(); + if (rootSurfaceControl == null) { + Log.w(TAG, "setBackgroundBlurRadius: rootSurfaceControl is null"); + return; + } + SurfaceControl surfaceControl = dragLayerViewRoot.getSurfaceControl(); + if (surfaceControl == null || !surfaceControl.isValid()) { + Log.w(TAG, "setBackgroundBlurRadius: surfaceControl is null or invalid"); + return; + } + Log.v(TAG, "setBackgroundBlurRadius: " + radius); + final SurfaceControl.Transaction transaction = + new SurfaceControl.Transaction().setBackgroundBlurRadius(surfaceControl, radius); + + try (transaction) { + // Set early wake-up flags when we know we're executing an expensive operation, this way + // SurfaceFlinger will adjust its internal offsets to avoid jank. + boolean wantsEarlyWakeUp = radius > 0 && radius < mMaxBlurRadius; + if (wantsEarlyWakeUp && !mInEarlyWakeUp) { + Log.d(TAG, "setBackgroundBlurRadius: setting early wakeup with token " + + mEarlyWakeupInfo); + Trace.instantForTrack(TRACE_TAG_APP, TAG, "notifyRendererForGpuLoadUp"); + dragLayerViewRoot.notifyRendererForGpuLoadUp("setBackgroundBlurRadius"); + try { + transaction.setEarlyWakeupStart(mEarlyWakeupInfo); + } catch (NoSuchMethodError e) { + // LC-Ignored: wtf? + } + mInEarlyWakeUp = true; + } else if (!wantsEarlyWakeUp && mInEarlyWakeUp) { + Log.d(TAG, "setBackgroundBlurRadius: clearing early wakeup with token " + + mEarlyWakeupInfo); + try { + transaction.setEarlyWakeupEnd(mEarlyWakeupInfo); + } catch (NoSuchMethodError e) { + // LC-Ignored: wtf? + } + mInEarlyWakeUp = false; + } + + rootSurfaceControl.applyTransactionOnDraw(transaction); + } + } + + boolean isBackgroundBlurEnabled() { + return BlurUtils.supportsBlursOnWindows() + && CrossWindowBlurListeners.getInstance().isCrossWindowBlurEnabled(); + } + /** * Proxy view connecting taskbar drag layer to the overlay window. * @@ -216,6 +328,13 @@ public final class TaskbarOverlayController { @Override protected void handleClose(boolean animate) { if (!mIsOpen) return; + if (ENABLE_TASKBAR_OVERFLOW.isTrue()) { + // Mark the view closed before attempting to remove it, so the drag layer does not + // schedule another call to close. Needed for taskbar overflow in case the KQS + // view shown for taskbar overflow needs to be reshown - delayed close call would + // would result in reshown KQS view getting hidden. + mIsOpen = false; + } mTaskbarContext.getDragLayer().removeView(this); Optional.ofNullable(mOverlayContext).ifPresent(c -> { if (canCloseWindow()) { diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java index 773b0b956f..9ecffdc6b3 100644 --- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java @@ -34,7 +34,6 @@ import com.android.app.viewcapture.ViewCaptureFactory; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; -import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.BaseDragLayer; @@ -72,8 +71,9 @@ public class TaskbarOverlayDragLayer extends @Override public void recreateControllers() { + super.recreateControllers(); List controllers = new ArrayList<>(); - controllers.add(mActivity.getDragController()); + controllers.add(mContainer.getDragController()); controllers.addAll(mTouchControllers); mControllers = controllers.toArray(new TouchController[0]); } @@ -87,7 +87,7 @@ public class TaskbarOverlayDragLayer extends @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) { - AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); + AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer); if (topView != null && topView.canHandleBack()) { topView.onBackInvoked(); return true; @@ -96,7 +96,7 @@ public class TaskbarOverlayDragLayer extends && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) { // Ignore escape if pressed in conjunction with any modifier keys. Close each // floating view one at a time for each key press. - AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); + AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer); if (topView != null) { topView.close(/* animate= */ true); return true; @@ -107,7 +107,7 @@ public class TaskbarOverlayDragLayer extends @Override public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { - if (mActivity.isAnySystemDragInProgress()) { + if (mContainer.isAnySystemDragInProgress()) { inoutInfo.touchableRegion.setEmpty(); inoutInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); } @@ -123,7 +123,7 @@ public class TaskbarOverlayDragLayer extends @Override public void onViewRemoved(View child) { super.onViewRemoved(child); - mActivity.getOverlayController().maybeCloseWindow(); + mContainer.getOverlayController().maybeCloseWindow(); } /** Adds a {@link TouchController} to this drag layer. */ @@ -147,14 +147,14 @@ public class TaskbarOverlayDragLayer extends * 2) Sets tappableInsets bottom inset to 0. */ private WindowInsets updateInsetsDueToStashing(WindowInsets oldInsets) { - if (!DisplayController.isTransientTaskbar(mActivity)) { + if (!mContainer.isTransientTaskbar()) { return oldInsets; } WindowInsets.Builder updatedInsetsBuilder = new WindowInsets.Builder(oldInsets); Insets oldNavInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars()); Insets newNavInsets = Insets.of(oldNavInsets.left, oldNavInsets.top, oldNavInsets.right, - mActivity.getStashedTaskbarHeight()); + mContainer.getStashedTaskbarHeight()); updatedInsetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets); Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement()); diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java deleted file mode 100644 index 14d391ba1c..0000000000 --- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.uioverrides; - -import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; -import static com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT; -import static com.android.app.animation.Interpolators.FINAL_FRAME; -import static com.android.app.animation.Interpolators.INSTANT; -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y; -import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW; -import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; -import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS; -import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; -import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; -import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA; - -import android.util.FloatProperty; -import android.view.animation.Interpolator; - -import androidx.annotation.NonNull; - -import com.android.launcher3.LauncherState; -import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.statemanager.StateManager.StateHandler; -import com.android.launcher3.states.StateAnimationConfig; -import com.android.quickstep.views.RecentsView; - -/** - * State handler for recents view. Manages UI changes and animations for recents view based off the - * current {@link LauncherState}. - * - * @param the recents view - */ -public abstract class BaseRecentsViewStateController - implements StateHandler { - protected final T mRecentsView; - protected final QuickstepLauncher mLauncher; - - public BaseRecentsViewStateController(@NonNull QuickstepLauncher launcher) { - mLauncher = launcher; - mRecentsView = launcher.getOverviewPanel(); - } - - @Override - public void setState(@NonNull LauncherState state) { - float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher); - RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]); - ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, scaleAndOffset[1]); - TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f); - - getContentAlphaProperty().set(mRecentsView, state.isRecentsViewVisible ? 1f : 0); - getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness()); - RECENTS_GRID_PROGRESS.set(mRecentsView, - state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f); - TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, state.showTaskThumbnailSplash() ? 1f : 0f); - } - - @Override - public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config, - PendingAnimation builder) { - if (config.hasAnimationFlag(SKIP_OVERVIEW)) { - return; - } - setStateWithAnimationInternal(toState, config, builder); - builder.addEndListener(success -> { - if (!success) { - mRecentsView.reset(); - } - }); - } - - /** - * Core logic for animating the recents view UI. - * - * @param toState state to animate to - * @param config current animation config - * @param setter animator set builder - */ - void setStateWithAnimationInternal(@NonNull final LauncherState toState, - @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) { - float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher); - setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0], - config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR)); - setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1], - config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR)); - setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f, - config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR)); - - boolean exitingOverview = - !FeatureFlags.enableSplitContextually() && !toState.isRecentsViewVisible; - if (mRecentsView.isSplitSelectionActive() && exitingOverview) { - setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController() - .createPlaceholderDismissAnim(mLauncher, LAUNCHER_SPLIT_SELECTION_EXIT_HOME, - setter.getDuration())); - setter.setViewAlpha( - mRecentsView.getSplitInstructionsView(), - 0, - config.getInterpolator( - ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE, - LINEAR - ) - ); - } - - setter.setFloat(mRecentsView, getContentAlphaProperty(), - toState.isRecentsViewVisible ? 1 : 0, - config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT)); - - setter.setFloat( - mRecentsView, getTaskModalnessProperty(), - toState.getOverviewModalness(), - config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR)); - - LauncherState fromState = mLauncher.getStateManager().getState(); - setter.setFloat(mRecentsView, TASK_THUMBNAIL_SPLASH_ALPHA, - toState.showTaskThumbnailSplash() ? 1f : 0f, - getOverviewInterpolator(fromState, toState)); - - setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS, - toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f, - getOverviewInterpolator(fromState, toState)); - } - - private Interpolator getOverviewInterpolator(LauncherState fromState, LauncherState toState) { - return fromState == QUICK_SWITCH_FROM_HOME - ? ACCELERATE_DECELERATE - : toState.isRecentsViewVisible ? INSTANT : FINAL_FRAME; - } - - abstract FloatProperty getTaskModalnessProperty(); - - /** - * Get property for content alpha for the recents view. - * - * @return the float property for the view's content alpha - */ - abstract FloatProperty getContentAlphaProperty(); -} diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index f91569824a..eec0009437 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -16,17 +16,16 @@ package com.android.launcher3.uioverrides; import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; -import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; +import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.Keyframe; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; -import android.annotation.Nullable; import android.content.Context; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; @@ -36,24 +35,22 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.os.Process; import android.util.AttributeSet; import android.util.FloatProperty; -import android.view.LayoutInflater; -import android.view.ViewGroup; +import android.util.Log; +import android.util.Property; import androidx.core.graphics.ColorUtils; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherSettings; +import com.android.launcher3.Flags; import com.android.launcher3.R; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.celllayout.DelegatedCellDrawing; -import com.android.launcher3.icons.BitmapInfo; -import com.android.launcher3.icons.GraphicsUtils; -import com.android.launcher3.icons.IconNormalizer; +import com.android.launcher3.graphics.ThemeManager; +import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -62,10 +59,6 @@ import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.DoubleShadowBubbleTextView; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import app.lawnchair.theme.color.tokens.ColorTokens; /** @@ -73,33 +66,54 @@ import app.lawnchair.theme.color.tokens.ColorTokens; */ public class PredictedAppIcon extends DoubleShadowBubbleTextView { + private static final float RING_SCALE_START_VALUE = 0.75f; private static final int RING_SHADOW_COLOR = 0x99000000; - private static final float RING_EFFECT_RATIO = 0.095f; - + private static final float RING_EFFECT_RATIO = Flags.enableLauncherIconShapes() ? 0.1f : 0.095f; private static final long ICON_CHANGE_ANIM_DURATION = 360; private static final long ICON_CHANGE_ANIM_STAGGER = 50; + private static final Property RING_SCALE_PROPERTY = + new Property<>(Float.TYPE, "ringScale") { + @Override + public Float get(PredictedAppIcon icon) { + return icon.mRingScale; + } + + @Override + public void set(PredictedAppIcon icon, Float value) { + icon.mRingScale = value; + icon.invalidate(); + } + }; + boolean mIsDrawingDot = false; private final DeviceProfile mDeviceProfile; private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Path mRingPath = new Path(); private final int mNormalizedIconSize; - private final Path mShapePath; + private Path mShapePath; private final Matrix mTmpMatrix = new Matrix(); private final BlurMaskFilter mShadowFilter; private boolean mIsPinned = false; - private int mPlateColor; + private final AnimColorHolder mPlateColor = new AnimColorHolder(); boolean mDrawForDrag = false; - // Used for the "slot-machine" education animation. - private List mSlotMachineIcons; - private Animator mSlotMachineAnim; + // Used for the "slot-machine" animation when prediction changes. + private final Rect mSlotIconBound = new Rect(0, 0, getIconSize(), getIconSize()); + private Drawable mSlotMachineIcon; private float mSlotMachineIconTranslationY; - private static final FloatProperty SLOT_MACHINE_TRANSLATION_Y = new FloatProperty( - "slotMachineTranslationY") { + // Used to animate the "ring" around predicted icons + private float mRingScale = 1f; + private boolean mForceHideRing = false; + private Animator mRingScaleAnim; + + private int mWidth; + + private static final FloatProperty SLOT_MACHINE_TRANSLATION_Y = + new FloatProperty("slotMachineTranslationY") { @Override public void setValue(PredictedAppIcon predictedAppIcon, float transY) { predictedAppIcon.mSlotMachineIconTranslationY = transY; @@ -120,50 +134,39 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { this(context, attrs, 0); } - Context mContext; - public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mContext = context; mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile(); - mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize()); + mNormalizedIconSize = Math.round(getIconSize() * ICON_VISIBLE_AREA_FACTOR); int shadowSize = context.getResources().getDimensionPixelSize( R.dimen.blur_size_thin_outline); mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER); - mShapePath = GraphicsUtils.getShapePath(context, mNormalizedIconSize); + mShapePath = ThemeManager.INSTANCE.get(context).getIconShape().getPath(mNormalizedIconSize); } @Override public void onDraw(Canvas canvas) { int count = canvas.save(); - boolean isSlotMachineAnimRunning = mSlotMachineAnim != null; + boolean isSlotMachineAnimRunning = mSlotMachineIcon != null; if (!mIsPinned) { - drawEffect(canvas); + drawRingEffect(canvas); if (isSlotMachineAnimRunning) { // Clip to to outside of the ring during the slot machine animation. canvas.clipPath(mRingPath); } - canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO); - canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO); - } - if (isSlotMachineAnimRunning) { - drawSlotMachineIcons(canvas); - } else { - super.onDraw(canvas); + canvas.scale(1 - 2f * RING_EFFECT_RATIO, 1 - 2f * RING_EFFECT_RATIO, + getWidth() * .5f, getHeight() * .5f); + if (isSlotMachineAnimRunning) { + canvas.translate(0, mSlotMachineIconTranslationY); + mSlotMachineIcon.setBounds(mSlotIconBound); + mSlotMachineIcon.draw(canvas); + canvas.translate(0, getSlotMachineIconPlusSpacingSize()); + } } + super.onDraw(canvas); canvas.restoreToCount(count); } - private void drawSlotMachineIcons(Canvas canvas) { - canvas.translate((getWidth() - getIconSize()) / 2f, - (getHeight() - getIconSize()) / 2f + mSlotMachineIconTranslationY); - for (Drawable icon : mSlotMachineIcons) { - icon.setBounds(0, 0, getIconSize(), getIconSize()); - icon.draw(canvas); - canvas.translate(0, getSlotMachineIconPlusSpacingSize()); - } - } - private float getSlotMachineIconPlusSpacingSize() { return getIconSize() + getOutlineOffsetY(); } @@ -179,118 +182,95 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { mIsDrawingDot = false; } - @Override - public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) { - // Create the slot machine animation first, since it uses the current icon to - // start. - Animator slotMachineAnim = animate - ? createSlotMachineAnim(Collections.singletonList(info.bitmap), false) - : null; - super.applyFromWorkspaceItem(info, animate, staggerIndex); - int oldPlateColor = mPlateColor; + /** + * Returns whether the newInfo differs from the current getTag(). + */ + private boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) { + boolean changedIcons = getTag() instanceof WorkspaceItemInfo oldInfo + && oldInfo.getTargetComponent() != null + && newInfo.getTargetComponent() != null + && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent()); + return changedIcons && isShown(); + } - int newPlateColor; + @Override + public void applyIconAndLabel(ItemInfoWithIcon info) { + super.applyIconAndLabel(info); if (getIcon().isThemed()) { - newPlateColor = ColorTokens.PredictedPlateColor.resolveColor(getContext()); + mPlateColor.endColor = ColorTokens.PredictedPlateColor.resolveColor(getContext()); } else { float[] hctPlateColor = new float[3]; ColorUtils.colorToM3HCT(mDotParams.appColor, hctPlateColor); - newPlateColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85); + mPlateColor.endColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85); } + mPlateColor.onUpdate(); + } + + /** + * Tries to apply the icon with animation and returns true if the icon was indeed animated + */ + public boolean applyFromWorkspaceItemWithAnimation(WorkspaceItemInfo info, int staggerIndex) { + boolean animate = shouldAnimateIconChange(info); + Drawable oldIcon = getIcon(); + int oldPlateColor = mPlateColor.currentColor; + applyFromWorkspaceItem(info); + + setContentDescription( + mIsPinned ? info.contentDescription : + getContext().getString(R.string.hotseat_prediction_content_description, + info.contentDescription)); if (!animate) { - mPlateColor = newPlateColor; - } - if (mIsPinned) { - setContentDescription(info.contentDescription); + mPlateColor.startColor = mPlateColor.endColor; + mPlateColor.progress.value = 1; + mPlateColor.onUpdate(); } else { - setContentDescription( - getContext().getString(R.string.hotseat_prediction_content_description, - info.contentDescription)); - } + mPlateColor.startColor = oldPlateColor; + mPlateColor.progress.value = 0; + mPlateColor.onUpdate(); - if (animate) { - ValueAnimator plateColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), - oldPlateColor, newPlateColor); - plateColorAnim.addUpdateListener(valueAnimator -> { - mPlateColor = (int) valueAnimator.getAnimatedValue(); - invalidate(); - }); AnimatorSet changeIconAnim = new AnimatorSet(); - if (slotMachineAnim != null) { + + ObjectAnimator plateColorAnim = + ObjectAnimator.ofFloat(mPlateColor.progress, AnimatedFloat.VALUE, 0, 1); + plateColorAnim.setAutoCancel(true); + changeIconAnim.play(plateColorAnim); + + if (!mIsPinned && oldIcon != null) { + // Play the slot machine icon + mSlotMachineIcon = oldIcon; + + float finalTrans = -getSlotMachineIconPlusSpacingSize(); + Keyframe[] keyframes = new Keyframe[] { + Keyframe.ofFloat(0f, 0f), + Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot + Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position + }; + keyframes[1].setInterpolator(ACCELERATE_DECELERATE); + keyframes[2].setInterpolator(ACCELERATE_DECELERATE); + + ObjectAnimator slotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes)); + slotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> { + mSlotMachineIcon = null; + mSlotMachineIconTranslationY = 0; + invalidate(); + })); + slotMachineAnim.setAutoCancel(true); changeIconAnim.play(slotMachineAnim); } - changeIconAnim.play(plateColorAnim); + changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER); changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start(); } - } - - /** - * Returns an Animator that translates the given icons in a "slot-machine" - * fashion, beginning - * and ending with the original icon. - */ - public @Nullable Animator createSlotMachineAnim(List iconsToAnimate) { - return createSlotMachineAnim(iconsToAnimate, true); - } - - /** - * Returns an Animator that translates the given icons in a "slot-machine" - * fashion, beginning - * with the original icon, then cycling through the given icons, optionally - * ending back with - * the original icon. - * - * @param endWithOriginalIcon Whether we should land back on the icon we started - * with, rather - * than the last item in iconsToAnimate. - */ - public @Nullable Animator createSlotMachineAnim(List iconsToAnimate, - boolean endWithOriginalIcon) { - if (mIsPinned || iconsToAnimate == null || iconsToAnimate.isEmpty()) { - return null; - } - if (mSlotMachineAnim != null) { - mSlotMachineAnim.end(); - } - - // Bookend the other animating icons with the original icon on both ends. - mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2); - mSlotMachineIcons.add(getIcon()); - iconsToAnimate.stream() - .map(iconInfo -> iconInfo.newIcon(mContext)) - .forEach(mSlotMachineIcons::add); - if (endWithOriginalIcon) { - mSlotMachineIcons.add(getIcon()); - } - - float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1); - Keyframe[] keyframes = new Keyframe[] { - Keyframe.ofFloat(0f, 0f), - Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot - Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position - }; - keyframes[1].setInterpolator(ACCELERATE_DECELERATE); - keyframes[2].setInterpolator(ACCELERATE_DECELERATE); - - mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this, - PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes)); - mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> { - mSlotMachineIcons = null; - mSlotMachineAnim = null; - mSlotMachineIconTranslationY = 0; - invalidate(); - })); - return mSlotMachineAnim; + return animate; } /** * Removes prediction ring from app icon */ public void pin(WorkspaceItemInfo info) { - if (mIsPinned) - return; + if (mIsPinned) return; mIsPinned = true; applyFromWorkspaceItem(info); setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE); @@ -298,16 +278,6 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { invalidate(); } - /** - * prepares prediction icon for usage after bind - */ - public void finishBinding(OnLongClickListener longClickListener) { - setOnLongClickListener(longClickListener); - ((CellLayoutLayoutParams) getLayoutParams()).canReorder = false; - setTextVisibility(false); - verifyHighRes(); - } - @Override public void getIconBounds(Rect outBounds) { super.getIconBounds(outBounds); @@ -322,7 +292,13 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { } private int getOutlineOffsetX() { - return (getMeasuredWidth() - mNormalizedIconSize) / 2; + int measuredWidth = getMeasuredWidth(); + if (mDisplay != DISPLAY_TASKBAR) { + Log.d("b/387844520", "getOutlineOffsetX: measured width = " + measuredWidth + + ", mNormalizedIconSize = " + mNormalizedIconSize + + ", last updated width = " + mWidth); + } + return (mWidth - mNormalizedIconSize) / 2; } private int getOutlineOffsetY() { @@ -335,6 +311,11 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); + mWidth = w; + mSlotIconBound.offsetTo((w - getIconSize()) / 2, (h - getIconSize()) / 2); + if (mDisplay != DISPLAY_TASKBAR) { + Log.d("b/387844520", "calling updateRingPath from onSizeChanged"); + } updateRingPath(); } @@ -345,18 +326,16 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { } private void updateRingPath() { - boolean isBadged = false; - if (getTag() instanceof WorkspaceItemInfo) { - WorkspaceItemInfo info = (WorkspaceItemInfo) getTag(); - isBadged = !Process.myUserHandle().equals(info.user) - || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; - } - + mShapePath = ThemeManager.INSTANCE.get(mContext) + .getIconShape() + .getPath(mNormalizedIconSize); mRingPath.reset(); + mTmpMatrix.reset(); mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY()); - mRingPath.addPath(mShapePath, mTmpMatrix); - if (isBadged) { + + FastBitmapDrawable icon = getIcon(); + if (icon != null && icon.getBadge() != null) { float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO; float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO); float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize; @@ -366,19 +345,73 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize); mRingPath.addPath(mShapePath, mTmpMatrix); } + invalidate(); } - private void drawEffect(Canvas canvas) { - // Don't draw ring effect if item is about to be dragged. - if (mDrawForDrag) { + @Override + public void setForceHideRing(boolean forceHideRing) { + if (mForceHideRing == forceHideRing) { + return; + } + mForceHideRing = forceHideRing; + + if (forceHideRing) { + invalidate(); + } else { + animateRingScale(RING_SCALE_START_VALUE, 1); + } + } + + private void cancelRingScaleAnim() { + if (mRingScaleAnim != null) { + mRingScaleAnim.cancel(); + } + } + + private void animateRingScale(float... ringScale) { + cancelRingScaleAnim(); + mRingScaleAnim = ObjectAnimator.ofFloat(this, RING_SCALE_PROPERTY, ringScale); + mRingScaleAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mRingScaleAnim = null; + } + }); + mRingScaleAnim.start(); + } + + private void drawRingEffect(Canvas canvas) { + // Don't draw ring effect if item is about to be dragged or if the icon is not visible. + if (mDrawForDrag || !mIsIconVisible || mForceHideRing) { return; } mIconRingPaint.setColor(RING_SHADOW_COLOR); mIconRingPaint.setMaskFilter(mShadowFilter); + int count = canvas.save(); + if (Flags.enableLauncherIconShapes()) { + // Scale canvas properly to for ring to be inner stroke and not exceed bounds. + // Since STROKE draws half on either side of Path, scale canvas down by 1x stroke ratio. + canvas.scale( + mRingScale * (1f - RING_EFFECT_RATIO), + mRingScale * (1f - RING_EFFECT_RATIO), + getWidth() / 2f, + getHeight() / 2f); + } else if (Float.compare(1, mRingScale) != 0) { + canvas.scale(mRingScale, mRingScale, getWidth() / 2f, getHeight() / 2f); + } + // Draw ring shadow around canvas. canvas.drawPath(mRingPath, mIconRingPaint); - mIconRingPaint.setColor(mPlateColor); + mIconRingPaint.setColor(mPlateColor.currentColor); + if (Flags.enableLauncherIconShapes()) { + mIconRingPaint.setStrokeWidth(getWidth() * RING_EFFECT_RATIO); + // Using FILL_AND_STROKE as there is still some gap to fill, + // between inner curve of ring / outer curve of icon. + mIconRingPaint.setStyle(Paint.Style.FILL_AND_STROKE); + } mIconRingPaint.setMaskFilter(null); + // Draw ring around canvas. canvas.drawPath(mRingPath, mIconRingPaint); + canvas.restoreToCount(count); } @Override @@ -414,17 +447,19 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView { }; } - /** - * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo - */ - public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) { - PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext()) - .inflate(R.layout.predicted_app_icon, parent, false); - icon.applyFromWorkspaceItem(info); - Launcher launcher = Launcher.getLauncher(parent.getContext()); - icon.setOnClickListener(launcher.getItemOnClickListener()); - icon.setOnFocusChangeListener(launcher.getFocusHandler()); - return icon; + private class AnimColorHolder { + + public final AnimatedFloat progress = new AnimatedFloat(this::onUpdate, 1); + public final ArgbEvaluator evaluator = ArgbEvaluator.getInstance(); + public Integer startColor = 0; + public Integer endColor = 0; + + public int currentColor = 0; + + private void onUpdate() { + currentColor = (Integer) evaluator.evaluate(progress.value, startColor, endColor); + invalidate(); + } } /** diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java deleted file mode 100644 index 45813ce52e..0000000000 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.uioverrides; - -import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; - -import android.appwidget.AppWidgetHost; -import android.appwidget.AppWidgetProviderInfo; -import android.content.Context; -import android.os.Looper; - -import androidx.annotation.NonNull; - -import com.android.launcher3.LauncherAppState; -import com.android.launcher3.util.Executors; -import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; -import com.android.launcher3.widget.LauncherWidgetHolder; - -import java.util.function.IntConsumer; - -/** - * {@link AppWidgetHost} that is used to receive the changes to the widgets without - * storing any {@code Activity} info like that of the launcher. - */ -final class QuickstepAppWidgetHost extends AppWidgetHost { - private final @NonNull Context mContext; - private final @NonNull IntConsumer mAppWidgetRemovedCallback; - private final @NonNull LauncherWidgetHolder.ProviderChangedListener mProvidersChangedListener; - - QuickstepAppWidgetHost(@NonNull Context context, @NonNull IntConsumer appWidgetRemovedCallback, - @NonNull LauncherWidgetHolder.ProviderChangedListener listener, - @NonNull Looper looper) { - super(context, APPWIDGET_HOST_ID, null, looper); - mContext = context; - mAppWidgetRemovedCallback = appWidgetRemovedCallback; - mProvidersChangedListener = listener; - } - - @Override - protected void onProvidersChanged() { - mProvidersChangedListener.notifyWidgetProvidersChanged(); - } - - @Override - public void onAppWidgetRemoved(int appWidgetId) { - // Route the call via model thread, in case it comes while a loader-bind is in progress - Executors.MODEL_EXECUTOR.execute( - () -> Executors.MAIN_EXECUTOR.execute( - () -> mAppWidgetRemovedCallback.accept(appWidgetId))); - } - - @Override - protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) { - LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo( - mContext, appWidget); - super.onProviderChanged(appWidgetId, info); - // The super method updates the dimensions of the providerInfo. Update the - // launcher spans accordingly. - info.initSpans(mContext, LauncherAppState.getIDP(mContext)); - } -} diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHostProvider.kt b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHostProvider.kt new file mode 100644 index 0000000000..7a8aa4b1f5 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHostProvider.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides + +import android.app.ActivityThread +import android.content.Context +import android.content.ContextWrapper +import com.android.launcher3.BuildConfigs +import com.android.launcher3.util.LooperExecutor +import com.android.launcher3.widget.LauncherWidgetHolder +import com.android.launcher3.widget.ListenableAppWidgetHost + +object QuickstepAppWidgetHostProvider { + + /** Static widget host which is always listening and is lazily created */ + @JvmStatic + val staticQuickstepHost: ListenableAppWidgetHost by lazy { + ListenableAppWidgetHost( + LooperContext( + ActivityThread.currentApplication(), + ListenableAppWidgetHost.widgetHolderExecutor, + ), + LauncherWidgetHolder.APPWIDGET_HOST_ID, + ) + .apply { if (BuildConfigs.WIDGETS_ENABLED) startListening() } + } + + private class LooperContext(ctx: Context, val executor: LooperExecutor) : ContextWrapper(ctx) { + + override fun getMainLooper() = executor.looper + + override fun getMainExecutor() = executor + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java index aa0cc1f0f0..516cb2b2c9 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java @@ -23,7 +23,6 @@ import android.app.ActivityTaskManager; import android.app.IActivityTaskManagerHidden; import android.app.PendingIntent; import android.content.Intent; -import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.Pair; @@ -31,20 +30,19 @@ import android.view.View; import android.widget.RemoteViews; import android.window.SplashScreen; -import com.android.launcher3.Utilities; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.widget.LauncherAppWidgetHostView; -import app.lawnchair.LawnchairApp; -import dev.rikka.tools.refine.Refine; +import java.util.function.Consumer; -/** - * Provides a Quickstep specific animation when launching an activity from an - * app widget. - */ -public class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { +import dev.rikka.tools.refine.Refine; +import app.lawnchair.LawnchairApp; + +/** Provides a Quickstep specific animation when launching an activity from an app widget. */ +class QuickstepInteractionHandler implements RemoteViews.InteractionHandler, + Consumer { private static final String TAG = "QuickstepInteractionHandler"; @@ -54,10 +52,15 @@ public class QuickstepInteractionHandler implements RemoteViews.InteractionHandl mLauncher = launcher; } + @Override + public void accept(LauncherAppWidgetHostView host) { + host.setInteractionHandler(this); + } + @SuppressWarnings("NewApi") @Override public boolean onInteraction(View view, PendingIntent pendingIntent, - RemoteViews.RemoteResponse remoteResponse) { + RemoteViews.RemoteResponse remoteResponse) { LauncherAppWidgetHostView hostView = findHostViewAncestor(view); if (hostView == null) { Log.e(TAG, "View did not have a LauncherAppWidgetHostView ancestor."); @@ -72,8 +75,14 @@ public class QuickstepInteractionHandler implements RemoteViews.InteractionHandl return true; } Pair options = remoteResponse.getLaunchOptions(view); - ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager() - .getActivityLaunchOptions(hostView); + + ActivityOptionsWrapper activityOptions = null; + try { + activityOptions = mLauncher.getAppTransitionManager() + .getActivityLaunchOptions(hostView, (ItemInfo) hostView.getTag()); + } catch (NullPointerException e) { + Log.e("pE(C7evQZDJ)", "Failed to get activity launch options"); + } if (!pendingIntent.isActivity()) { // In the event this pending intent eventually launches an activity, i.e. a trampoline, // use the Quickstep transition animation. @@ -89,7 +98,8 @@ public class QuickstepInteractionHandler implements RemoteViews.InteractionHandl pendingIntent.getCreatorPackage(), activityOptions.options.getRemoteAnimationAdapter()); } - } catch (RemoteException e) { + } catch (NullPointerException | RemoteException e) { + // pE-TODO(C7evQZDJ): Remove NullPointerException after fixing // Do nothing. } } @@ -101,16 +111,24 @@ public class QuickstepInteractionHandler implements RemoteViews.InteractionHandl } catch (Throwable t) { // ignore } - options = Pair.create(options.first, activityOptions.options); + // pE-TODO(C7evQZDJ): Remove activityOptions null check + if (activityOptions != null) { + options = Pair.create(options.first, activityOptions.options); + } if (pendingIntent.isActivity()) { logAppLaunch(hostView.getTag()); } - return RemoteViews.startPendingIntent(hostView, pendingIntent, options); + if (activityOptions != null) { + return RemoteViews.startPendingIntent(hostView, pendingIntent, options); + } else { + Log.d("pE(C7evQZDJ)", "activityOptions is null!"); + return RemoteViews.startPendingIntent(hostView, pendingIntent, + remoteResponse.getLaunchOptions(view)); + } } /** * Logs that the app was launched from the widget. - * * @param itemInfo the widget info. */ private void logAppLaunch(Object itemInfo) { @@ -123,8 +141,7 @@ public class QuickstepInteractionHandler implements RemoteViews.InteractionHandl private LauncherAppWidgetHostView findHostViewAncestor(View v) { while (v != null) { - if (v instanceof LauncherAppWidgetHostView) - return (LauncherAppWidgetHostView) v; + if (v instanceof LauncherAppWidgetHostView) return (LauncherAppWidgetHostView) v; v = (View) v.getParent(); } return null; diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 03aa100608..2b67c232be 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -16,35 +16,46 @@ package com.android.launcher3.uioverrides; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE; import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE; -import static com.android.launcher3.Flags.enablePredictiveBackGesture; +import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion; +import static com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur; import static com.android.launcher3.Flags.enableUnfoldStateAnimation; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE; import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON; import static com.android.launcher3.LauncherSettings.Animation.VIEW_BACKGROUND; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.FLAG_SKIP_STATE_ANNOUNCEMENT; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.NO_OFFSET; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK; import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; +import static com.android.launcher3.Utilities.ATLEAST_BAKLAVA; +import static com.android.launcher3.Utilities.ATLEAST_S_V2; +import static com.android.launcher3.Utilities.ATLEAST_T; +import static com.android.launcher3.Utilities.isRtl; import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; -import static com.android.launcher3.config.FeatureFlags.enableSplitContextually; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED; import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition; import static com.android.launcher3.popup.SystemShortcut.APP_INFO; +import static com.android.launcher3.popup.SystemShortcut.BUBBLE_SHORTCUT; import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP; import static com.android.launcher3.popup.SystemShortcut.INSTALL; import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL; +import static com.android.launcher3.popup.SystemShortcut.REMOVE; import static com.android.launcher3.popup.SystemShortcut.UNINSTALL_APP; import static com.android.launcher3.popup.SystemShortcut.WIDGETS; import static com.android.launcher3.taskbar.LauncherTaskbarUIController.ALL_APPS_PAGE_PROGRESS_INDEX; @@ -60,9 +71,7 @@ import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback; import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY; -import static com.android.window.flags2.Flags.enableDesktopWindowingMode; -import static com.android.window.flags2.Flags.enableDesktopWindowingWallpaperActivity; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -71,6 +80,7 @@ import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.graphics.RectF; @@ -78,16 +88,16 @@ import android.hardware.display.DisplayManager; import android.media.permission.SafeCloseable; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; import android.os.IRemoteCallback; -import android.os.Looper; import android.os.SystemProperties; +import android.os.UserHandle; +import android.text.TextUtils; import android.util.AttributeSet; -import android.view.Display; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.View; +import android.widget.AnalogClock; +import android.widget.TextClock; import android.window.BackEvent; import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedDispatcher; @@ -97,22 +107,24 @@ import android.window.SplashScreen; import androidx.annotation.BinderThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.android.app.viewcapture.ViewCaptureFactory; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; +import com.android.launcher3.GestureNavContract; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherState; -import com.android.launcher3.OnBackPressedHandler; import com.android.launcher3.QuickstepAccessibilityDelegate; import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.allapps.AllAppsRecyclerView; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.apppairs.AppPairIcon; @@ -123,9 +135,9 @@ import com.android.launcher3.hybridhotseat.HotseatPredictionController; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.WellbeingModel; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.proxy.ProxyActivityStarter; import com.android.launcher3.statehandlers.DepthController; @@ -134,9 +146,9 @@ import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.taskbar.TaskbarManager; +import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; -import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepHolderFactory; import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory; import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController; import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController; @@ -144,7 +156,10 @@ import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTou import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController; import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController; -import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewDismissTouchController; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewLaunchTouchController; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewRecentsTouchContext; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchControllerDeprecated; import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController; import com.android.launcher3.util.ActivityOptionsWrapper; @@ -152,26 +167,33 @@ import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.ObjectWrapper; +import com.android.launcher3.util.OverviewCommandHelperProtoLogProxy; import com.android.launcher3.util.PendingRequestArgs; import com.android.launcher3.util.PendingSplitSelectInfo; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; +import com.android.launcher3.util.StableViewInfo; import com.android.launcher3.util.StartActivityParams; import com.android.launcher3.util.TouchController; -import com.android.launcher3.widget.LauncherWidgetHolder; +import com.android.launcher3.views.FloatingIconView; +import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.OverviewComponentObserver; -import com.android.quickstep.RecentsAnimationDeviceState; +import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener; import com.android.quickstep.RecentsModel; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.TouchInteractionService.TISBinder; -import com.android.quickstep.util.GroupTask; +import com.android.quickstep.fallback.window.RecentsWindowFlags; +import com.android.quickstep.fallback.window.RecentsWindowManager; +import com.android.quickstep.util.ActiveGestureProtoLogProxy; +import com.android.quickstep.util.AsyncClockEventDelegate; import com.android.quickstep.util.LauncherUnfoldAnimationController; import com.android.quickstep.util.QuickstepOnboardingPrefs; import com.android.quickstep.util.SplitSelectStateController; +import com.android.quickstep.util.SplitTask; import com.android.quickstep.util.SplitToWorkspaceController; import com.android.quickstep.util.SplitWithKeyboardShortcutController; import com.android.quickstep.util.TISBindHelper; @@ -182,7 +204,9 @@ import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; import com.android.quickstep.views.TaskView; +import com.android.systemui.animation.back.FlingOnBackAnimationCallback; import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.unfold.RemoteUnfoldSharedComponent; import com.android.systemui.unfold.UnfoldTransitionFactory; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; @@ -191,9 +215,11 @@ import com.android.systemui.unfold.config.UnfoldTransitionConfig; import com.android.systemui.unfold.dagger.UnfoldMain; import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver; import com.android.systemui.unfold.updates.RotationChangeProvider; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopState; -import app.lawnchair.LawnchairApp; -import app.lawnchair.compat.LawnchairQuickstepCompat; import kotlin.Unit; import java.io.FileDescriptor; @@ -208,19 +234,21 @@ import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Stream; -public class QuickstepLauncher extends Launcher implements RecentsViewContainer { +import app.lawnchair.LawnchairApp; +import app.lawnchair.compat.LawnchairQuickstepCompat; + +public class QuickstepLauncher extends Launcher implements RecentsViewContainer, + SystemShortcut.BubbleActivityStarter { private static final boolean TRACE_LAYOUTS = SystemProperties.getBoolean("persist.debug.trace_layouts", false); private static final String TRACE_RELAYOUT_CLASS = SystemProperties.get("persist.debug.trace_request_layout_class", null); - public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false; protected static final String RING_APPEAR_ANIMATION_PREFIX = "RingAppearAnimation\t"; - private FixedContainerItems mAllAppsPredictions; + private PredictedContainerInfo mAllAppsPredictions; private HotseatPredictionController mHotseatPredictionController; private DepthController mDepthController; - private @Nullable DesktopVisibilityController mDesktopVisibilityController; private QuickstepTransitionManager mAppTransitionManager; private OverviewActionsView mActionsView; @@ -233,6 +261,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer private SplitSelectStateController mSplitSelectStateController; private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController; private SplitToWorkspaceController mSplitToWorkspaceController; + private BubbleBarLocation mBubbleBarLocation; /** * If Launcher restarted while in the middle of an Overview split select, it needs this data to @@ -249,35 +278,58 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer private boolean mIsPredictiveBackToHomeInProgress; - public static QuickstepLauncher getLauncher(Context context) { - return fromContext(context); - } + private boolean mCanShowAllAppsEducationView; + + private boolean mIsOverlayVisible; + + private final OverviewChangeListener mOverviewChangeListener = this::onOverviewTargetChanged; + + private boolean mOverviewBlurEnabled; + + private final TaskViewRecentsTouchContext mTaskViewRecentsTouchContext = + new TaskViewRecentsTouchContext() { + @Override + public boolean isRecentsInteractive() { + return isInState(OVERVIEW) || isInState(OVERVIEW_MODAL_TASK); + } + + @Override + public boolean isRecentsModal() { + return isInState(OVERVIEW_MODAL_TASK); + } + + @Override + public void onUserControlledAnimationCreated( + AnimatorPlaybackController animController) { + getStateManager().setCurrentUserControlledAnimation(animController); + } + }; @Override protected void setupViews() { + getAppWidgetHolder().setOnViewCreationCallback(new QuickstepInteractionHandler(this)); + mDepthController = new DepthController(this); + mOverviewBlurEnabled = isOverviewBackgroundBlurEnabled(); + getTheme().applyStyle(getOverviewBlurStyleResId(), true); super.setupViews(); mActionsView = findViewById(R.id.overview_actions_view); - RecentsView overviewPanel = getOverviewPanel(); + RecentsView overviewPanel = getOverviewPanel(); SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this); mSplitSelectStateController = - new SplitSelectStateController(this, mHandler, getStateManager(), + new SplitSelectStateController(this, getStateManager(), getDepthController(), getStatsLogManager(), systemUiProxy, RecentsModel.INSTANCE.get(this), () -> onStateBack()); - RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(asContext()); - // TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher - OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver( - asContext(), deviceState); - if (enableDesktopWindowingMode() && LawnchairApp.isRecentsEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(this) && LawnchairApp.isRecentsEnabled()) { mDesktopRecentsTransitionController = new DesktopRecentsTransitionController( getStateManager(), systemUiProxy, LawnchairApp.getInstance().getIApplicationThread(), getDepthController()); } overviewPanel.init(mActionsView, mSplitSelectStateController, mDesktopRecentsTransitionController); - mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this, - mSplitSelectStateController, overviewComponentObserver, deviceState); + mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController( + this, mSplitSelectStateController); mSplitToWorkspaceController = new SplitToWorkspaceController(this, mSplitSelectStateController); mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize()); @@ -290,12 +342,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } mTISBindHelper = new TISBindHelper(this, this::onTISConnected); - mDepthController = new DepthController(this); - if (enableDesktopWindowingMode()) { - mDesktopVisibilityController = new DesktopVisibilityController(this); - mDesktopVisibilityController.registerSystemUiListener(); - mSplitSelectStateController.initSplitFromDesktopController(this, - overviewComponentObserver); + + if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(this)) { + mSplitSelectStateController.initSplitFromDesktopController(this); } mHotseatPredictionController = new HotseatPredictionController(this); @@ -309,7 +358,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public void logAppLaunch(StatsLogManager statsLogManager, ItemInfo info, - InstanceId instanceId) { + InstanceId instanceId) { // If the app launch is from any of the surfaces in AllApps then add the InstanceId from // LiveSearchManager to recreate the AllApps session on the server side. if (mAllAppsSessionLogId != null && ALL_APPS.equals( @@ -322,16 +371,16 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer if (mAllAppsPredictions != null && (info.itemType == ITEM_TYPE_APPLICATION || info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) { - int count = mAllAppsPredictions.items.size(); + List items = mAllAppsPredictions.getContents(); + int count = items.size(); for (int i = 0; i < count; i++) { - ItemInfo targetInfo = mAllAppsPredictions.items.get(i); + ItemInfo targetInfo = items.get(i); if (targetInfo.itemType == info.itemType && targetInfo.user.equals(info.user) && Objects.equals(targetInfo.getIntent(), info.getIntent())) { logger.withRank(i); break; } - } } logger.log(LAUNCHER_APP_LAUNCH_TAP); @@ -341,7 +390,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override protected void completeAddShortcut(Intent data, int container, int screenId, int cellX, - int cellY, PendingRequestArgs args) { + int cellY, PendingRequestArgs args) { if (container == CONTAINER_HOTSEAT) { mHotseatPredictionController.onDeferredDrop(cellX, cellY); } @@ -381,31 +430,19 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public RunnableList startActivitySafely(View v, Intent intent, ItemInfo item) { - // Only pause is taskbar controller is not present until the transition (if it exists) ends - mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null); PredictionRowView predictionRowView = getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class); // Pause the prediction row updates until the transition (if it exists) ends. predictionRowView.setPredictionUiUpdatePaused(true); RunnableList result = super.startActivitySafely(v, intent, item); if (result == null) { - mHotseatPredictionController.setPauseUIUpdate(false); predictionRowView.setPredictionUiUpdatePaused(false); } else { - result.add(() -> { - mHotseatPredictionController.setPauseUIUpdate(false); - predictionRowView.setPredictionUiUpdatePaused(false); - }); + result.add(() -> predictionRowView.setPredictionUiUpdatePaused(false)); } return result; } - @Override - public void startBinding() { - super.startBinding(); - mHotseatPredictionController.verifyUIUpdateNotPaused(); - } - @Override protected void onActivityFlagsChanged(int changeBits) { if ((changeBits & ACTIVITY_STATE_STARTED) != 0) { @@ -431,9 +468,34 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer super.showAllAppsFromIntent(alreadyOnHome); } + @Override + public boolean isAllAppsBackgroundBlurEnabled() { + return mDepthController != null && mDepthController.isCrossWindowBlursEnabled() + && Flags.allAppsBlur(); + } + + @Override + public boolean isOverviewBackgroundBlurEnabled() { + return mDepthController != null && mDepthController.isCrossWindowBlursEnabled() + && enableOverviewBackgroundWallpaperBlur(); + } + + /** Apply the blur or blur fallback style to the current theme. */ + public void updateBlurStyle() { + if (enableOverviewBackgroundWallpaperBlur()) { + if (isOverviewBackgroundBlurEnabled() != mOverviewBlurEnabled) { + mWallpaperThemeManager.recreateToUpdateTheme(); + } + } else if (Flags.allAppsBlur()) { + // For all apps, we only need to update the scrim, which draws the panel. But if the + // activity was recreated above, this is unnecessary. + getAppsView().invalidateHeader(); + } + } + protected void onItemClicked(View view) { if (!mSplitToWorkspaceController.handleSecondAppSelectionForSplit(view)) { - QuickstepLauncher.super.getItemOnClickListener().onClick(view); + super.getItemOnClickListener().onClick(view); } } @@ -443,27 +505,34 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } @Override - public Stream getSupportedShortcuts() { + public Stream getSupportedShortcuts(int container) { // Order matters as it affects order of appearance in popup container List shortcuts = new ArrayList(Arrays.asList( APP_INFO, WellbeingModel.SHORTCUT_FACTORY, mHotseatPredictionController)); + shortcuts.addAll(getSplitShortcuts()); shortcuts.add(WIDGETS); shortcuts.add(INSTALL); + if (Flags.enableLongPressRemoveShortcut() + && (container == CONTAINER_HOTSEAT || container == CONTAINER_DESKTOP + || /* Folder */ container > 0)) { + shortcuts.add(REMOVE); + } + shortcuts.add(DONT_SUGGEST_APP); if (Flags.enablePrivateSpaceInstallShortcut()) { shortcuts.add(PRIVATE_PROFILE_INSTALL); } - if (Flags.enableShortcutDontSuggestApp()) { - shortcuts.add(DONT_SUGGEST_APP); - } if (Flags.enablePrivateSpace()) { shortcuts.add(UNINSTALL_APP); } + if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + shortcuts.add(BUBBLE_SHORTCUT); + } return shortcuts.stream(); } private List> getSplitShortcuts() { - if (!mDeviceProfile.isTablet || mSplitSelectStateController.isSplitSelectActive()) { + if (!mDeviceProfile.getDeviceProperties().isTablet() || mSplitSelectStateController.isSplitSelectActive()) { return Collections.emptyList(); } RecentsView recentsView = getOverviewPanel(); @@ -486,11 +555,10 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer boolean started = ((getActivityFlags() & ACTIVITY_STATE_STARTED)) != 0; if (started) { DeviceProfile profile = getDeviceProfile(); - boolean willUserBeActive = - (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0; boolean visible = (state == NORMAL || state == OVERVIEW) - && (willUserBeActive || isUserActive()) - && !profile.isVerticalBarLayout(); + && isUserActive() + && !profile.isVerticalBarLayout() + && !mIsOverlayVisible; SystemUiProxy.INSTANCE.get(this) .setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx); } @@ -500,17 +568,26 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } @Override - public void bindExtraContainerItems(FixedContainerItems item) { - if (item.containerId == Favorites.CONTAINER_PREDICTION) { - mAllAppsPredictions = item; - PredictionRowView predictionRowView = - getAppsView().getFloatingHeaderView().findFixedRowByType( - PredictionRowView.class); - predictionRowView.setPredictedApps(item.items); - } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { - mHotseatPredictionController.setPredictedItems(item); - } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) { - getPopupDataProvider().setRecommendedWidgets(item.items); + public void onOverlayVisibilityChanged(boolean visible) { + super.onOverlayVisibilityChanged(visible); + mIsOverlayVisible = visible; + } + + @Override + public void bindPredictedContainerInfo(PredictedContainerInfo info) { + super.bindPredictedContainerInfo(info); + switch (info.id) { + case Favorites.CONTAINER_ALL_APPS_PREDICTION: + mAllAppsPredictions = info; + getAppsView().getFloatingHeaderView().findFixedRowByType( + PredictionRowView.class).setPredictedApps(info.getContents()); + break; + case Favorites.CONTAINER_HOTSEAT_PREDICTION: + mHotseatPredictionController.setPredictedItems(info); + break; + case Favorites.CONTAINER_WIDGETS_PREDICTION: + getWidgetPickerDataProvider().setWidgetRecommendations(info.getContents()); + break; } } @@ -533,23 +610,25 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer mUnfoldTransitionProgressProvider.destroy(); } + OverviewComponentObserver.INSTANCE.get(this) + .removeOverviewChangeListener(mOverviewChangeListener); mTISBindHelper.onDestroy(); if (mLauncherUnfoldAnimationController != null) { mLauncherUnfoldAnimationController.onDestroy(); } - if (mDesktopVisibilityController != null) { - mDesktopVisibilityController.unregisterSystemUiListener(); - } - if (mSplitSelectStateController != null) { mSplitSelectStateController.onDestroy(); } + RecentsView recentsView = getOverviewPanel(); + if (recentsView != null) { + recentsView.destroy(); + } + super.onDestroy(); mHotseatPredictionController.destroy(); - mSplitWithKeyboardShortcutController.onDestroy(); if (mViewCapture != null) mViewCapture.close(); if (Utilities.ATLEAST_U) { removeBackAnimationCallback(mSplitSelectStateController.getSplitBackHandler()); @@ -583,9 +662,16 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } case QUICK_SWITCH_STATE_ORDINAL: { RecentsView rv = getOverviewPanel(); - TaskView tasktolaunch = rv.getCurrentPageTaskView(); - if (tasktolaunch != null) { - tasktolaunch.launchTask(success -> { + TaskView currentPageTask = rv.getCurrentPageTaskView(); + TaskView fallbackTask = rv.getFirstTaskView(); + if (currentPageTask != null || fallbackTask != null) { + TaskView taskToLaunch = currentPageTask; + if (currentPageTask == null) { + taskToLaunch = fallbackTask; + ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFallback( + rv.getCurrentPage()); + } + taskToLaunch.launchWithoutAnimation(success -> { if (!success) { getStateManager().goToState(OVERVIEW); } else { @@ -594,6 +680,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer return Unit.INSTANCE; }); } else { + ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFailed(rv.getCurrentPage()); getStateManager().goToState(NORMAL); } break; @@ -602,6 +689,15 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } } + @Override + protected void setTitle(@NonNull LauncherState state) { + if (state.hasFlag(FLAG_SKIP_STATE_ANNOUNCEMENT)) { + // Prevent accessibility title update announcement + getWindow().getAttributes().accessibilityTitle = getString(state.getTitle()); + } + super.setTitle(state); + } + @Override public TouchController[] createTouchControllers() { NavigationMode mode = DisplayController.getNavigationMode(this); @@ -636,11 +732,17 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer break; } - if (!getDeviceProfile().isMultiWindowMode) { - list.add(new StatusBarTouchController(this)); + if (!getDeviceProfile().getDeviceProperties().isMultiWindowMode()) { + list.add(new StatusBarTouchController( + this, () -> this.isInState(LauncherState.NORMAL))); } - list.add(new LauncherTaskViewController(this)); + if (enableExpressiveDismissTaskMotion()) { + list.add(new TaskViewLaunchTouchController<>(this, mTaskViewRecentsTouchContext)); + list.add(new TaskViewDismissTouchController<>(this, mTaskViewRecentsTouchContext)); + } else { + list.add(new TaskViewTouchControllerDeprecated<>(this, mTaskViewRecentsTouchContext)); + } return list.toArray(new TouchController[list.size()]); } @@ -649,28 +751,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer return new QuickstepAtomicAnimationFactory(this); } - @Override - protected LauncherWidgetHolder createAppWidgetHolder() { - final QuickstepHolderFactory factory = - (QuickstepHolderFactory) LauncherWidgetHolder.HolderFactory.newFactory(this); - return factory.newInstance(this, - appWidgetId -> getWorkspace().removeWidget(appWidgetId), - new QuickstepInteractionHandler(this)); - } - @Override protected void onCreate(Bundle savedInstanceState) { - // Back dispatcher is registered in {@link BaseActivity#onCreate}. For predictive back to - // work, we must opt-in BEFORE registering back dispatcher. So we need to call - // setEnableOnBackInvokedCallback() before super.onCreate() - if (Utilities.ATLEAST_U && enablePredictiveBackGesture()) { - try { - getApplicationInfo().setEnableOnBackInvokedCallback(true); - } catch (NoSuchMethodError e) { - // Ignore - } - - } super.onCreate(savedInstanceState); if (savedInstanceState != null) { mPendingSplitSelectInfo = ObjectWrapper.unwrap( @@ -678,21 +760,29 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } addMultiWindowModeChangedListener(mDepthController); initUnfoldTransitionProgressProvider(); - if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { - mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow()); - } + mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow()); // getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE); QuickstepOnboardingPrefs.setup(this); // View.setTraceLayoutSteps(TRACE_LAYOUTS); // View.setTracedRequestLayoutClassClass(TRACE_RELAYOUT_CLASS); + OverviewComponentObserver.INSTANCE.get(this) + .addOverviewChangeListener(mOverviewChangeListener); } @Override protected boolean initDeviceProfile(InvariantDeviceProfile idp) { final boolean ret = super.initDeviceProfile(idp); -// mDeviceProfile.isPredictiveBackSwipe = -// getApplicationInfo().isOnBackInvokedCallbackEnabled(); - mDeviceProfile.isPredictiveBackSwipe = false; + try { + mDeviceProfile.isPredictiveBackSwipe = + getApplicationInfo().isOnBackInvokedCallbackEnabled(); + } catch (Throwable t) { + mDeviceProfile.isPredictiveBackSwipe = false; + } + if (ATLEAST_S_V2) { + if (ret) { + SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx); + } + } return ret; } @@ -702,7 +792,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer // Check if there is already an instance of this app running, if so, initiate the split // using that. mSplitSelectStateController.findLastActiveTasksAndRunCallback( - Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()), + Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()), false /* findExactPairMatch */, foundTasks -> { @Nullable Task foundTask = foundTasks[0]; @@ -710,11 +800,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer splitSelectSource.alreadyRunningTaskId = taskWasFound ? foundTask.key.id : INVALID_TASK_ID; - if (enableSplitContextually()) { - startSplitToHome(splitSelectSource); - } else { - recentsView.initiateSplitSelect(splitSelectSource); - } + startSplitToHome(splitSelectSource); } ); } @@ -729,7 +815,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer Rect tempRect = new Rect(); mSplitSelectStateController.setInitialTaskSelect(source.intent, - source.position.stagePosition, source.itemInfo, source.splitEvent, + source.position.stagePosition, source.getItemInfo(), source.splitEvent, source.alreadyRunningTaskId); RecentsView recentsView = getOverviewPanel(); @@ -747,6 +833,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer floatingTaskView.setOnClickListener(view -> mSplitSelectStateController.getSplitAnimationController(). playAnimPlaceholderToFullscreen(this, view, Optional.empty())); + floatingTaskView.setContentDescription(source.getItemInfo().contentDescription); + mSplitSelectStateController.setFirstFloatingTaskView(floatingTaskView); anim.addListener(new AnimatorListenerAdapter() { @Override @@ -788,6 +876,10 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer if (mLauncherUnfoldAnimationController != null) { mLauncherUnfoldAnimationController.onResume(); } + + if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) { + mTaskbarUIController.onLauncherResume(); + } } @Override @@ -798,25 +890,50 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer super.onPause(); - if (enableSplitContextually()) { - // If Launcher pauses before both split apps are selected, exit split screen. - if (!mSplitSelectStateController.isBothSplitAppsConfirmed() && - !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) { - mSplitSelectStateController - .logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED); - mSplitSelectStateController.getSplitAnimationController() - .playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED); - } + // If Launcher pauses before both split apps are selected, exit split screen. + if (!mSplitSelectStateController.isBothSplitAppsConfirmed() && + !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) { + mSplitSelectStateController + .logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED); + mSplitSelectStateController.getSplitAnimationController() + .playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED); + } + + if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) { + mTaskbarUIController.onLauncherPause(); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) { + mTaskbarUIController.onLauncherStop(); } } @Override protected void onNewIntent(Intent intent) { + boolean intentHasGnc = GestureNavContract.canBuildFromIntent(intent); super.onNewIntent(intent); OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper(); if (overviewCommandHelper != null) { overviewCommandHelper.clearPendingCommands(); } + if (RecentsWindowFlags.getEnableOverviewInWindow() && !intentHasGnc) { + RecentsWindowManager defaultRecentsWindowManager = + RecentsWindowManager.REPOSITORY_INSTANCE.get(this).get(DEFAULT_DISPLAY); + if (defaultRecentsWindowManager != null) { + defaultRecentsWindowManager.cleanupRecentsWindow(); + } + } + } + + @Override + protected void logOnNewIntent(boolean alreadyOnHome, boolean shouldMoveToDefaultScreen, + String action, boolean internalStateHandled) { + OverviewCommandHelperProtoLogProxy.logOnNewIntent(alreadyOnHome, shouldMoveToDefaultScreen, + action, internalStateHandled); } public QuickstepTransitionManager getAppTransitionManager() { @@ -835,7 +952,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override protected void handleGestureContract(Intent intent) { - if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) { + if (GestureNavContract.isContractEnabled(intent) + && (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get() + || RecentsWindowFlags.getEnableOverviewInWindow())) { super.handleGestureContract(intent); } } @@ -865,7 +984,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer onTaskbarInAppDisplayProgressUpdate(progress, WIDGETS_PAGE_PROGRESS_INDEX); if (mEnableWidgetDepth) { getDepthController().widgetDepth.setValue(Utilities.mapToRange( - progress, 0f, 1f, 0f, getDeviceProfile().bottomSheetDepth, EMPHASIZED)); + progress, 0f, 1f, 0f, + getDeviceProfile().getBottomSheetProfile().getBottomSheetDepth(), EMPHASIZED)); } } @@ -881,71 +1001,68 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer // event won't go through ViewRootImpl#InputStage#onProcess. // So when receive back key, try to do the same check thing in // ViewRootImpl#NativePreImeInputStage#onProcess - if (!Utilities.ATLEAST_U || !enablePredictiveBackGesture() - || event.getKeyCode() != KeyEvent.KEYCODE_BACK + if (event.getKeyCode() != KeyEvent.KEYCODE_BACK || event.getAction() != KeyEvent.ACTION_UP || event.isCanceled()) { return false; } + // Lawnchair-TODO-Merge: LC disabled this, 16r2 enabled it. // getOnBackAnimationCallback().onBackInvoked(); return true; } @Override protected void registerBackDispatcher() { - if (!enablePredictiveBackGesture()) { - super.registerBackDispatcher(); - return; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (Utilities.ATLEAST_U) { getOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, - new OnBackAnimationCallback() { - - @Nullable - OnBackPressedHandler mActiveOnBackPressedHandler; + new FlingOnBackAnimationCallback() { + + @Nullable OnBackAnimationCallback mActiveOnBackAnimationCallback; @Override - public void onBackStarted(@NonNull BackEvent backEvent) { - if (mActiveOnBackPressedHandler != null) { - mActiveOnBackPressedHandler.onBackCancelled(); + public void onBackStartedCompat(@NonNull BackEvent backEvent) { + if (mActiveOnBackAnimationCallback != null) { + mActiveOnBackAnimationCallback.onBackCancelled(); } - mActiveOnBackPressedHandler = getOnBackPressedHandler(); - mActiveOnBackPressedHandler.onBackStarted(); + mActiveOnBackAnimationCallback = getOnBackAnimationCallback(); + mActiveOnBackAnimationCallback.onBackStarted(backEvent); } - + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Override - public void onBackInvoked() { - // Recreate mActiveOnBackPressedHandler if necessary to avoid NPE + public void onBackInvokedCompat() { + // Recreate mActiveOnBackAnimationCallback if necessary to avoid NPE // because: // 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not // called on ACTION_DOWN before onBackInvoked() is called in ACTION_UP. // 2. Launcher#onBackPressed() will call onBackInvoked() without calling // onBackInvoked() beforehand. - if (mActiveOnBackPressedHandler == null) { - mActiveOnBackPressedHandler = getOnBackPressedHandler(); + if (mActiveOnBackAnimationCallback == null) { + mActiveOnBackAnimationCallback = getOnBackAnimationCallback(); } - mActiveOnBackPressedHandler.onBackInvoked(); - mActiveOnBackPressedHandler = null; + mActiveOnBackAnimationCallback.onBackInvoked(); + mActiveOnBackAnimationCallback = null; TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onBackInvoked"); } + @Override - public void onBackProgressed(@NonNull BackEvent backEvent) { + public void onBackProgressedCompat(@NonNull BackEvent backEvent) { if (!FeatureFlags.IS_STUDIO_BUILD - && mActiveOnBackPressedHandler == null) { + && mActiveOnBackAnimationCallback == null) { return; } - mActiveOnBackPressedHandler.onBackProgressed(backEvent.getProgress()); + mActiveOnBackAnimationCallback.onBackProgressed(backEvent); } - + @Override - public void onBackCancelled() { + public void onBackCancelledCompat() { if (!FeatureFlags.IS_STUDIO_BUILD - && mActiveOnBackPressedHandler == null) { + && mActiveOnBackAnimationCallback == null) { return; } - mActiveOnBackPressedHandler.onBackCancelled(); - mActiveOnBackPressedHandler = null; + mActiveOnBackAnimationCallback.onBackCancelled(); + mActiveOnBackAnimationCallback = null; } }); } @@ -963,7 +1080,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public void startIntentSenderForResult(IntentSender intent, int requestCode, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) { + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) { if (requestCode != -1) { mPendingActivityRequestCode = requestCode; StartActivityParams params = new StartActivityParams(this, requestCode); @@ -995,13 +1112,16 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public void setResumed() { - if (!enableDesktopWindowingWallpaperActivity() - && mDesktopVisibilityController != null - && mDesktopVisibilityController.areDesktopTasksVisible() - && !mDesktopVisibilityController.isRecentsGestureInProgress()) { - // Return early to skip setting activity to appear as resumed - // TODO: b/333533253 - Remove after flag rollout - return; + DesktopVisibilityController desktopVisibilityController = + DesktopVisibilityController.INSTANCE.get(this); + if (ATLEAST_BAKLAVA) { + if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() + && desktopVisibilityController.isInDesktopModeAndNotInOverview(getDisplayId()) + && !desktopVisibilityController.isRecentsGestureInProgress()) { + // Return early to skip setting activity to appear as resumed + // TODO: b/333533253 - Remove after flag rollout + return; + } } super.setResumed(); } @@ -1023,6 +1143,13 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } } + private void onOverviewTargetChanged(boolean isHomeAndOverviewSame) { + QuickstepTransitionManager transitionManager = getAppTransitionManager(); + if (transitionManager != null) { + transitionManager.onOverviewTargetChange(); + } + } + private void onTISConnected(TISBinder binder) { TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager(); if (taskbarManager != null) { @@ -1053,7 +1180,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer /** Receives animation progress from sysui process. */ private void initRemotelyCalculatedUnfoldAnimation(UnfoldTransitionConfig config) { - RemoteUnfoldSharedComponent unfoldComponent = UnfoldTransitionFactory.createRemoteUnfoldSharedComponent( /* context= */ this, @@ -1081,7 +1207,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } private void initUnfoldAnimationController(UnfoldTransitionProgressProvider progressProvider, - @UnfoldMain RotationChangeProvider rotationChangeProvider) { + @UnfoldMain RotationChangeProvider rotationChangeProvider) { mLauncherUnfoldAnimationController = new LauncherUnfoldAnimationController( /* launcher= */ this, getWindowManager(), @@ -1090,14 +1216,32 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer ); } - public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) { - mTaskbarUIController = taskbarUIController; + @Override + public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) { + mTaskbarUIController = (LauncherTaskbarUIController) taskbarUIController; } + @Override public @Nullable LauncherTaskbarUIController getTaskbarUIController() { return mTaskbarUIController; } + /** Provides the translation X for the hotseat item. */ + public int getHotseatItemTranslationX(ItemInfo itemInfo) { + int translationX = 0; + if (isBubbleBarEnabled() && mBubbleBarLocation != null) { + boolean isBubblesOnLeft = mBubbleBarLocation.isOnLeft(isRtl(getResources())); + translationX += mDeviceProfile + .getHotseatTranslationXForNavBar(this, isBubblesOnLeft); + } + if (isBubbleBarEnabled() + && mDeviceProfile.shouldAdjustHotseatForBubbleBar(asContext(), hasBubbles())) { + translationX += (int) mDeviceProfile + .getHotseatAdjustedTranslation(asContext(), itemInfo.cellX); + } + return translationX; + } + public SplitToWorkspaceController getSplitToWorkspaceController() { return mSplitToWorkspaceController; } @@ -1138,11 +1282,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer return mDepthController; } - @Nullable - public DesktopVisibilityController getDesktopVisibilityController() { - return mDesktopVisibilityController; - } - @Nullable public UnfoldTransitionProgressProvider getUnfoldTransitionProgressProvider() { return mUnfoldTransitionProgressProvider; @@ -1173,23 +1312,27 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } } + @NonNull @Override public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) { - ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(v); + ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions( + v, item != null ? item : (ItemInfo) v.getTag()); if (mLastTouchUpTime > 0) { activityOptions.options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_LAUNCHER, mLastTouchUpTime); } - if (item != null && (item.animationType == DEFAULT_NO_ICON + if (ATLEAST_T) { + if (item != null && (item.animationType == DEFAULT_NO_ICON || item.animationType == VIEW_BACKGROUND)) { - activityOptions.options.setSplashScreenStyle( + activityOptions.options.setSplashScreenStyle( SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR); - } else { - activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON); + } else { + activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON); + } } activityOptions.options.setLaunchDisplayId( (v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId() - : Display.DEFAULT_DISPLAY); + : DEFAULT_DISPLAY); Utilities.allowBGLaunch(activityOptions.options); return activityOptions; } @@ -1198,9 +1341,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer public ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) { RunnableList callbacks = new RunnableList(); ActivityOptions options = ActivityOptions.makeCustomAnimation(this, 0, 0); - options.setSplashScreenStyle(splashScreenStyle); + if (ATLEAST_T) { + options.setSplashScreenStyle(splashScreenStyle); + } Utilities.allowBGLaunch(options); - IRemoteCallback endCallback = completeRunnableListCallback(callbacks); + IRemoteCallback endCallback = completeRunnableListCallback(callbacks, this); options.setOnAnimationAbortListener(endCallback); options.setOnAnimationFinishedListener(endCallback); return new ActivityOptionsWrapper(options, callbacks); @@ -1208,8 +1353,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override @BinderThread - public void enterStageSplitFromRunningApp(boolean leftOrTop) { - mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop); + public void enterStageSplitFromRunningApp(boolean leftOrTop, int displayId) { + mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop, displayId); } @Override @@ -1288,18 +1433,19 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer mTISBindHelper.setPredictiveBackToHomeInProgress(isInProgress); } - @Override - public boolean areDesktopTasksVisible() { - if (mDesktopVisibilityController != null) { - return mDesktopVisibilityController.areDesktopTasksVisible(); - } - return false; + public boolean getPredictiveBackToHomeInProgress() { + return mIsPredictiveBackToHomeInProgress; } @Override - protected void onDeviceProfileInitiated() { - super.onDeviceProfileInitiated(); - SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx); + public boolean areDesktopTasksVisible() { + return DesktopVisibilityController.INSTANCE.get(this) + .isInDesktopModeAndNotInOverview(getDisplayId()); + } + + @Override + public boolean shouldShowHomeBehindDesktop() { + return DesktopState.fromContext(this).getShouldShowHomeBehindDesktop(); } @Override @@ -1308,29 +1454,25 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx); TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager(); if (taskbarManager != null) { - taskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged"); + taskbarManager.debugPrimaryTaskbar("QuickstepLauncher#onDeviceProfileChanged", + true); } } /** - * Launches the given {@link GroupTask} in splitscreen. + * Launches the given {@link SplitTask} in splitscreen. */ public void launchSplitTasks( - @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { - // Top/left and bottom/right tasks respectively. - Task task1 = groupTask.task1; - // task2 should never be null when calling this method. Allow a crash to catch invalid calls - Task task2 = groupTask.task2; - mSplitSelectStateController.launchExistingSplitPair( - null /* launchingTaskView */, - task1.key.id, - task2.key.id, + @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) { + mSplitSelectStateController.launchExistingSplitPair(null /* launchingTaskView */, + splitTask.getTopLeftTask().key.id, + splitTask.getBottomRightTask().key.id, SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, /* callback= */ success -> mSplitSelectStateController.resetState(), /* freezeTaskList= */ false, - groupTask.mSplitBounds == null - ? SNAP_TO_50_50 - : groupTask.mSplitBounds.snapPosition, + splitTask.getSplitBounds() == null + ? SNAP_TO_2_50_50 + : splitTask.getSplitBounds().snapPosition, remoteTransition); } @@ -1338,8 +1480,14 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer * Launches two apps as an app pair. */ public void launchAppPair(AppPairIcon appPairIcon) { + // Potentially show the Taskbar education once the app pair launch finishes mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon, - CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE); + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE, + (success) -> { + if (success && mTaskbarUIController != null) { + mTaskbarUIController.showEduOnAppLaunch(); + } + }); } public boolean canStartHomeSafely() { @@ -1357,41 +1505,69 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer return (mTaskbarUIController != null && mTaskbarUIController.hasBubbles()); } - @NonNull - public TISBindHelper getTISBindHelper() { - return mTISBindHelper; - } - @Override public boolean handleIncorrectSplitTargetSelection() { - if (!enableSplitContextually() || !mSplitSelectStateController.isSplitSelectActive()) { + if (!mSplitSelectStateController.isSplitSelectActive()) { return false; } mSplitSelectStateController.getSplitInstructionsView().goBoing(); return true; } - private static final class LauncherTaskViewController extends - TaskViewTouchController { + @Override + public void showShortcutBubble(ShortcutInfo info) { + if (info == null) return; + SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info); + } - LauncherTaskViewController(QuickstepLauncher activity) { - super(activity); + @Override + public void showAppBubble(Intent intent, UserHandle user) { + if (intent == null || intent.getPackage() == null) return; + SystemUiProxy.INSTANCE.get(this).showAppBubble(intent, user); + } + + /** Sets the location of the bubble bar */ + public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { + mBubbleBarLocation = bubbleBarLocation; + } + + /** + * Similar to {@link #getFirstHomeElementForAppClose} but also matches all apps if its visible + */ + @Nullable + public View getFirstVisibleElementForAppClose( + @Nullable StableViewInfo svi, String packageName, UserHandle user) { + if (isInState(LauncherState.ALL_APPS)) { + AllAppsRecyclerView activeRecyclerView = getAppsView().getActiveRecyclerView(); + View v = null; + if (svi != null) { + // Preferred item match + v = activeRecyclerView.findViewByPredicate(view -> + view.isAggregatedVisible() + && view.getTag() instanceof ItemInfo info && svi.matches(info)); + } + if (v == null) { + // Package user match + v = activeRecyclerView.findViewByPredicate(view -> + view.isAggregatedVisible() && view.getTag() instanceof ItemInfo info + && info.itemType == ITEM_TYPE_APPLICATION + && info.user.equals(user) + && TextUtils.equals(info.getTargetPackage(), packageName)); + } + + if (v != null && activeRecyclerView.computeVerticalScrollOffset() > 0) { + RectF locationBounds = new RectF(); + FloatingIconView.getLocationBoundsForView(this, v, false, locationBounds, + new Rect()); + if (locationBounds.top < getAppsView().getHeaderBottom()) { + // Icon is covered by scrim, return null to play fallback animation. + return null; + } + } + return v; } - @Override - protected boolean isRecentsInteractive() { - return mContainer.isInState(OVERVIEW) || mContainer.isInState(OVERVIEW_MODAL_TASK); - } - - @Override - protected boolean isRecentsModal() { - return mContainer.isInState(OVERVIEW_MODAL_TASK); - } - - @Override - protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { - mContainer.getStateManager().setCurrentUserControlledAnimation(animController); - } + return getFirstHomeElementForAppClose(svi, packageName, user); } @Override @@ -1421,6 +1597,18 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + switch (name) { + case "TextClock", "android.widget.TextClock" -> { + TextClock tc = new TextClock(context, attrs); + tc.setClockEventDelegate(AsyncClockEventDelegate.INSTANCE.get(this)); + return tc; + } + case "AnalogClock", "android.widget.AnalogClock" -> { + AnalogClock ac = new AnalogClock(context, attrs); + ac.setClockEventDelegate(AsyncClockEventDelegate.INSTANCE.get(this)); + return ac; + } + } return super.onCreateView(parent, name, context, attrs); } @@ -1428,4 +1616,33 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer public boolean isRecentsViewVisible() { return getStateManager().getState().isRecentsViewVisible; } + + public boolean isCanShowAllAppsEducationView() { + return mCanShowAllAppsEducationView; + } + + public void setCanShowAllAppsEducationView(boolean canShowAllAppsEducationView) { + mCanShowAllAppsEducationView = canShowAllAppsEducationView; + } + + @Override + public void returnToHomescreen() { + getStateManager().goToState(LauncherState.NORMAL); + } + + @Override + public int getOverviewBlurStyleResId() { + return isOverviewBackgroundBlurEnabled() ? R.style.OverviewBlurStyle + : R.style.OverviewBlurFallbackStyle; + } + + @Override + public LauncherActivityInterface getContainerInterface() { + return LauncherActivityInterface.INSTANCE; + } + + @Override + public SplitSelectStateController getSplitSelectStateController() { + return mSplitSelectStateController; + } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java index 45398b0267..846c850a18 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java @@ -16,43 +16,43 @@ package com.android.launcher3.uioverrides; import static com.android.launcher3.BuildConfigs.WIDGETS_ENABLED; +import static com.android.launcher3.uioverrides.QuickstepAppWidgetHostProvider.getStaticQuickstepHost; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.launcher3.widget.ListenableAppWidgetHost.getWidgetHolderExecutor; +import android.appwidget.AppWidgetEvent; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; -import android.util.Log; import android.util.SparseArray; import android.widget.RemoteViews; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.LauncherWidgetHolder; -import java.util.ArrayList; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; + import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; -import java.util.function.IntConsumer; /** * {@link LauncherWidgetHolder} that puts the app widget host in the background */ -public class QuickstepWidgetHolder extends LauncherWidgetHolder { - - private static final String TAG = "QuickstepWidgetHolder"; +public final class QuickstepWidgetHolder extends LauncherWidgetHolder { private static final UpdateKey KEY_PROVIDER_UPDATE = AppWidgetHostView::onUpdateProviderInfo; @@ -60,52 +60,22 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { AppWidgetHostView::updateAppWidget; private static final UpdateKey KEY_VIEW_DATA_CHANGED = AppWidgetHostView::onViewDataChanged; + private static final UpdateKey KEY_COLLECT_WIDGET_EVENT = + (view, event) -> { + event.merge(view.collectWidgetEvent()); + }; - private static final List sHolders = new ArrayList<>(); private static final SparseArray sListeners = new SparseArray<>(); - private static AppWidgetHost sWidgetHost = null; - private final UpdateHandler mUpdateHandler = this::onWidgetUpdate; - private final @Nullable RemoteViews.InteractionHandler mInteractionHandler; - - private final @NonNull IntConsumer mAppWidgetRemovedCallback; // Map to all pending updated keyed with appWidgetId; private final SparseArray mPendingUpdateMap = new SparseArray<>(); - public QuickstepWidgetHolder(@NonNull Context context, - @Nullable IntConsumer appWidgetRemovedCallback, - @Nullable RemoteViews.InteractionHandler interactionHandler) { - super(context, appWidgetRemovedCallback); - mAppWidgetRemovedCallback = appWidgetRemovedCallback != null ? appWidgetRemovedCallback - : i -> {}; - mInteractionHandler = interactionHandler; - MAIN_EXECUTOR.execute(() -> sHolders.add(this)); - } - - @Override - @NonNull - protected AppWidgetHost createHost(@NonNull Context context, - @Nullable IntConsumer appWidgetRemovedCallback) { - if (sWidgetHost == null) { - sWidgetHost = new QuickstepAppWidgetHost(context.getApplicationContext(), - i -> MAIN_EXECUTOR.execute(() -> - sHolders.forEach(h -> h.mAppWidgetRemovedCallback.accept(i))), - () -> MAIN_EXECUTOR.execute(() -> - sHolders.forEach(h -> - // Listeners might remove themselves from the list during the - // iteration. Creating a copy of the list to avoid exceptions - // for concurrent modification. - new ArrayList<>(h.mProviderChangedListeners).forEach( - ProviderChangedListener::notifyWidgetProvidersChanged))), - UI_HELPER_EXECUTOR.getLooper()); - if (WIDGETS_ENABLED) { - sWidgetHost.startListening(); - } - } - return sWidgetHost; + @AssistedInject + public QuickstepWidgetHolder(@Assisted("UI_CONTEXT") @NonNull Context context) { + super(context, getStaticQuickstepHost()); } @Override @@ -169,21 +139,6 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { sListeners.remove(appWidgetId); } - /** - * Called when the launcher is destroyed - */ - @Override - public void destroy() { - try { - MAIN_EXECUTOR.submit(() -> { - clearViews(); - sHolders.remove(this); - }).get(); - } catch (Exception e) { - Log.e(TAG, "Failed to remove self from holder list", e); - } - } - @Override protected boolean shouldListen(int flags) { return (flags & (FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED)) @@ -199,12 +154,10 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { return; } - try { - sWidgetHost.setAppWidgetHidden(); - } catch (Throwable t) { - // Ignore - } - setListeningFlag(false); + getWidgetHolderExecutor().execute(() -> { + mWidgetHost.setAppWidgetHidden(); + setListeningFlag(false); + }); } @Override @@ -220,7 +173,7 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { }; QuickstepWidgetHolderListener holderListener = getHolderListener(appWidgetId); holderListener.addHolder(handler); - return () -> holderListener.mListeningHolders.remove(handler); + return () -> holderListener.removeHolder(handler); } /** @@ -242,7 +195,6 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { protected LauncherAppWidgetHostView createViewInternal( int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { LauncherAppWidgetHostView widgetView = new LauncherAppWidgetHostView(mContext); - widgetView.setInteractionHandler(mInteractionHandler); widgetView.setAppWidget(appWidgetId, appWidget); widgetView.updateAppWidget(getHolderListener(appWidgetId).addHolder(mUpdateHandler)); return widgetView; @@ -252,7 +204,7 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { QuickstepWidgetHolderListener listener = sListeners.get(appWidgetId); if (listener == null) { listener = new QuickstepWidgetHolderListener(appWidgetId); - sWidgetHost.setListener(appWidgetId, listener); + getStaticQuickstepHost().setListener(appWidgetId, listener); sListeners.put(appWidgetId, listener); } return listener; @@ -265,7 +217,7 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { public void clearViews() { mViews.clear(); for (int i = sListeners.size() - 1; i >= 0; i--) { - sListeners.valueAt(i).mListeningHolders.remove(mUpdateHandler); + sListeners.valueAt(i).removeHolder(mUpdateHandler); } } @@ -292,13 +244,15 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { mWidgetId = widgetId; } - @UiThread - @Nullable public RemoteViews addHolder(@NonNull UpdateHandler holder) { - mListeningHolders.add(holder); + MAIN_EXECUTOR.execute(() -> mListeningHolders.add(holder)); return mRemoteViews; } + public void removeHolder(@NonNull UpdateHandler holder) { + MAIN_EXECUTOR.execute(() -> mListeningHolders.remove(holder)); + } + @Override @AnyThread public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) { @@ -319,50 +273,38 @@ public class QuickstepWidgetHolder extends LauncherWidgetHolder { executeOnMainExecutor(KEY_VIEW_DATA_CHANGED, viewId); } + @Nullable + @Override + public AppWidgetEvent collectWidgetEvent() { + if (!android.appwidget.flags.Flags.engagementMetrics()) return null; + + CompletableFuture future = new CompletableFuture<>(); + MAIN_EXECUTOR.execute(() -> { + AppWidgetEvent.Builder event = new AppWidgetEvent.Builder(); + mListeningHolders.forEach(holder -> + holder.onWidgetUpdate(mWidgetId, KEY_COLLECT_WIDGET_EVENT, event)); + future.complete(event.isEmpty() ? null : event.build()); + }); + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + return null; + } + } + private void executeOnMainExecutor(UpdateKey key, T data) { MAIN_EXECUTOR.execute(() -> mListeningHolders.forEach(holder -> holder.onWidgetUpdate(mWidgetId, key, data))); } } - /** - * {@code HolderFactory} subclass that takes an interaction handler as one of the parameters - * when creating a new instance. - */ - public static class QuickstepHolderFactory extends HolderFactory { - @SuppressWarnings("unused") - public QuickstepHolderFactory(Context context) { } + /** A factory that generates new instances of {@code LauncherWidgetHolder} */ + @AssistedFactory + public interface QuickstepWidgetHolderFactory extends WidgetHolderFactory { @Override - public LauncherWidgetHolder newInstance(@NonNull Context context, - @Nullable IntConsumer appWidgetRemovedCallback) { - return newInstance(context, appWidgetRemovedCallback, null); - } - - /** - * @param context The context of the caller - * @param appWidgetRemovedCallback The callback that is called when widgets are removed - * @param interactionHandler The interaction handler when the widgets are clicked - * @return A new {@link LauncherWidgetHolder} instance - */ - public LauncherWidgetHolder newInstance(@NonNull Context context, - @Nullable IntConsumer appWidgetRemovedCallback, - @Nullable RemoteViews.InteractionHandler interactionHandler) { - - if (!FeatureFlags.ENABLE_WIDGET_HOST_IN_BACKGROUND.get()) { - return new LauncherWidgetHolder(context, appWidgetRemovedCallback) { - @Override - protected AppWidgetHost createHost(Context context, - @Nullable IntConsumer appWidgetRemovedCallback) { - AppWidgetHost host = super.createHost(context, appWidgetRemovedCallback); - host.setInteractionHandler(interactionHandler); - return host; - } - }; - } - return new QuickstepWidgetHolder(context, appWidgetRemovedCallback, interactionHandler); - } + QuickstepWidgetHolder newInstance(@Assisted("UI_CONTEXT") @NonNull Context context); } private interface UpdateKey extends BiConsumer { } diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java deleted file mode 100644 index 235ec7b101..0000000000 --- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.uioverrides; - -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON; -import static com.android.launcher3.LauncherState.OVERVIEW; -import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS; -import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; -import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE; -import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; -import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; -import static com.android.quickstep.views.RecentsView.TASK_MODALNESS; -import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION; -import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION; -import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL; -import static com.android.wm.shell.Flags.enableSplitContextual; - -import android.animation.AnimatorSet; -import android.annotation.TargetApi; -import android.os.Build; -import android.util.FloatProperty; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import com.android.launcher3.LauncherState; -import com.android.launcher3.Utilities; -import com.android.launcher3.anim.AnimatedFloat; -import com.android.launcher3.anim.AnimatorListeners; -import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.anim.PropertySetter; -import com.android.launcher3.states.StateAnimationConfig; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.util.AnimUtils; -import com.android.quickstep.util.SplitAnimationTimings; -import com.android.quickstep.views.ClearAllButton; -import com.android.quickstep.views.LauncherRecentsView; -import com.android.quickstep.views.RecentsView; - -/** - * State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing - * the basic view properties, this class also manages changes in the task visuals. - */ -@TargetApi(Build.VERSION_CODES.O) -public final class RecentsViewStateController extends - BaseRecentsViewStateController { - - public RecentsViewStateController(QuickstepLauncher launcher) { - super(launcher); - } - - @Override - public void setState(@NonNull LauncherState state) { - super.setState(state); - if (state.isRecentsViewVisible) { - mRecentsView.updateEmptyMessage(); - } else { - mRecentsView.resetTaskVisuals(); - } - setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state); - mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress()); - // In Overview, we may be layering app surfaces behind Launcher, so we need to notify - // DepthController to prevent optimizations which might occlude the layers behind - mLauncher.getDepthController().setHasContentBehindLauncher(state.isRecentsViewVisible); - - PendingAnimation builder = - new PendingAnimation(state.getTransitionDuration(mLauncher, true)); - - handleSplitSelectionState(state, builder, /* animate */false); - } - - @Override - void setStateWithAnimationInternal(@NonNull LauncherState toState, - @NonNull StateAnimationConfig config, @NonNull PendingAnimation builder) { - super.setStateWithAnimationInternal(toState, config, builder); - - if (toState.isRecentsViewVisible) { - // While animating into recents, update the visible task data as needed - builder.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL)); - mRecentsView.updateEmptyMessage(); - // TODO(b/246283207): Remove logging once root cause of flake detected. - if (Utilities.isRunningInTestHarness()) { - Log.d("b/246283207", "RecentsView#setStateWithAnimationInternal getCurrentPage(): " - + mRecentsView.getCurrentPage() - + ", getScrollForPage(getCurrentPage())): " - + mRecentsView.getScrollForPage(mRecentsView.getCurrentPage())); - } - } else { - builder.addListener( - AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals)); - } - // In Overview, we may be layering app surfaces behind Launcher, so we need to notify - // DepthController to prevent optimizations which might occlude the layers behind - builder.addListener(AnimatorListeners.forSuccessCallback(() -> - mLauncher.getDepthController().setHasContentBehindLauncher( - toState.isRecentsViewVisible))); - - handleSplitSelectionState(toState, builder, /* animate */true); - - setAlphas(builder, config, toState); - builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS, - toState.getOverviewFullscreenProgress(), LINEAR); - } - - /** - * Create or dismiss split screen select animations. - * @param builder if null then this will run the split select animations right away, otherwise - * will add animations to builder. - */ - private void handleSplitSelectionState(@NonNull LauncherState toState, - @NonNull PendingAnimation builder, boolean animate) { - boolean goingToOverviewFromWorkspaceContextual = enableSplitContextual() && - toState == OVERVIEW && mLauncher.isSplitSelectionActive(); - if (toState != OVERVIEW_SPLIT_SELECT && !goingToOverviewFromWorkspaceContextual) { - // Not going to split - return; - } - - // Create transition animations to split select - RecentsPagedOrientationHandler orientationHandler = - ((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler(); - Pair, FloatProperty> taskViewsFloat = - orientationHandler.getSplitSelectTaskOffset( - TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION, - mLauncher.getDeviceProfile()); - - SplitAnimationTimings timings = - AnimUtils.getDeviceOverviewToSplitTimings(mLauncher.getDeviceProfile().isTablet); - if (!goingToOverviewFromWorkspaceContextual) { - // This animation is already done for the contextual case, don't redo it - mRecentsView.createSplitSelectInitAnimation(builder, - toState.getTransitionDuration(mLauncher, true /* isToState */)); - } - // Shift tasks vertically downward to get out of placeholder view - builder.setFloat(mRecentsView, taskViewsFloat.first, - toState.getSplitSelectTranslation(mLauncher), - timings.getGridSlidePrimaryInterpolator()); - // Zero out horizontal translation - builder.setFloat(mRecentsView, taskViewsFloat.second, - 0, - timings.getGridSlideSecondaryInterpolator()); - - if (!animate) { - AnimatorSet as = builder.buildAnim(); - as.start(); - as.end(); - } - } - - private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config, - LauncherState state) { - float clearAllButtonAlpha = state.areElementsVisible(mLauncher, CLEAR_ALL_BUTTON) ? 1 : 0; - propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA, - clearAllButtonAlpha, LINEAR); - float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS) ? 1 : 0; - propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(), - AnimatedFloat.VALUE, overviewButtonAlpha, config.getInterpolator( - ANIM_OVERVIEW_ACTIONS_FADE, LINEAR)); - } - - @Override - FloatProperty getTaskModalnessProperty() { - return TASK_MODALNESS; - } - - @Override - FloatProperty getContentAlphaProperty() { - return CONTENT_ALPHA; - } -} diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt new file mode 100644 index 0000000000..927d94c2f6 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides + +import com.android.app.animation.Interpolators.ACCELERATE_DECELERATE +import com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT +import com.android.app.animation.Interpolators.FINAL_FRAME +import com.android.app.animation.Interpolators.INSTANT +import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.Flags.enableDesktopExplodedView +import com.android.launcher3.Flags.enableLargeDesktopWindowingTile +import com.android.launcher3.LauncherState +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.anim.AnimatorListeners.forSuccessCallback +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.anim.PropertySetter +import com.android.launcher3.statemanager.StateManager.StateHandler +import com.android.launcher3.states.StateAnimationConfig +import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE +import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE +import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL +import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE +import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X +import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y +import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW +import com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview +import com.android.quickstep.util.AnimUtils +import com.android.quickstep.views.AddDesktopButton +import com.android.quickstep.views.ClearAllButton +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET +import com.android.quickstep.views.RecentsView.CONTENT_ALPHA +import com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS +import com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS +import com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS +import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY +import com.android.quickstep.views.RecentsView.TASK_MODALNESS +import com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION +import com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION +import com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION +import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA +import com.android.quickstep.views.RecentsViewUtils.Companion.DESK_EXPLODE_PROGRESS +import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL + +/** + * State handler for handling UI changes for [com.android.quickstep.views.LauncherRecentsView]. In + * addition to managing the basic view properties, this class also manages changes in the task + * visuals. + */ +class RecentsViewStateController(private val launcher: QuickstepLauncher) : + StateHandler { + private val recentsView: RecentsView<*, *> = launcher.getOverviewPanel() + + override fun setState(state: LauncherState) { + val scaleAndOffset = state.getOverviewScaleAndOffset(launcher) + RECENTS_SCALE_PROPERTY.set(recentsView, scaleAndOffset[0]) + ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, scaleAndOffset[1]) + TASK_SECONDARY_TRANSLATION.set(recentsView, 0f) + + CONTENT_ALPHA.set(recentsView, if (state.isRecentsViewVisible) 1f else 0f) + TASK_MODALNESS.set(recentsView, state.overviewModalness) + RECENTS_GRID_PROGRESS.set( + recentsView, + if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f, + ) + if (enableDesktopExplodedView()) { + DESK_EXPLODE_PROGRESS.set(recentsView, if (state.showExplodedDesktopView()) 1f else 0f) + } + + TASK_THUMBNAIL_SPLASH_ALPHA.set( + recentsView, + if (state.showTaskThumbnailSplash()) 1f else 0f, + ) + if (enableLargeDesktopWindowingTile()) { + DESKTOP_CAROUSEL_DETACH_PROGRESS.set( + recentsView, + if (state.detachDesktopCarousel()) 1f else 0f, + ) + } + + if (state.isRecentsViewVisible) { + recentsView.updateEmptyMessage() + } else { + recentsView.resetTaskVisuals() + } + setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, StateAnimationConfig(), state) + recentsView.setFullscreenProgress(state.overviewFullscreenProgress) + // In Overview, we may be layering app surfaces behind Launcher, so we need to notify + // DepthController to prevent optimizations which might occlude the layers behind + launcher.depthController.setHasContentBehindLauncher(state.isRecentsViewVisible) + + val builder = PendingAnimation(state.getTransitionDuration(launcher, true).toLong()) + handleSplitSelectionState(state, builder, animate = false) + } + + override fun setStateWithAnimation( + toState: LauncherState, + config: StateAnimationConfig, + builder: PendingAnimation, + ) { + if (config.hasAnimationFlag(SKIP_OVERVIEW)) return + + val scaleAndOffset = toState.getOverviewScaleAndOffset(launcher) + builder.setFloat( + recentsView, + RECENTS_SCALE_PROPERTY, + scaleAndOffset[0], + config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR), + ) + builder.setFloat( + recentsView, + ADJACENT_PAGE_HORIZONTAL_OFFSET, + scaleAndOffset[1], + config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR), + ) + builder.setFloat( + recentsView, + TASK_SECONDARY_TRANSLATION, + 0f, + config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR), + ) + + builder.setFloat( + recentsView, + CONTENT_ALPHA, + if (toState.isRecentsViewVisible) 1f else 0f, + config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT), + ) + + builder.setFloat( + recentsView, + TASK_MODALNESS, + toState.overviewModalness, + config.getInterpolator( + ANIM_OVERVIEW_MODAL, + if (enableGridOnlyOverview() && !toState.isRecentsViewVisible) FINAL_FRAME + else LINEAR, + ), + ) + + val fromState = launcher.stateManager.state + builder.setFloat( + recentsView, + TASK_THUMBNAIL_SPLASH_ALPHA, + if (toState.showTaskThumbnailSplash()) 1f else 0f, + getOverviewInterpolator(fromState, toState), + ) + + builder.setFloat( + recentsView, + RECENTS_GRID_PROGRESS, + if (toState.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f, + getOverviewInterpolator(fromState, toState), + ) + + if (enableDesktopExplodedView()) { + builder.setFloat( + recentsView, + DESK_EXPLODE_PROGRESS, + if (toState.showExplodedDesktopView()) 1f else 0f, + getOverviewInterpolator(fromState, toState), + ) + } + + if (enableLargeDesktopWindowingTile()) { + builder.setFloat( + recentsView, + DESKTOP_CAROUSEL_DETACH_PROGRESS, + if (toState.detachDesktopCarousel()) 1f else 0f, + getOverviewInterpolator(fromState, toState), + ) + } + + if (toState.isRecentsViewVisible) { + // While animating into recents, update the visible task data as needed + builder.addOnFrameCallback { recentsView.loadVisibleTaskData(FLAG_UPDATE_ALL) } + recentsView.updateEmptyMessage() + } else { + builder.addListener(forSuccessCallback { recentsView.resetTaskVisuals() }) + } + // In Overview, we may be layering app surfaces behind Launcher, so we need to notify + // DepthController to prevent optimizations which might occlude the layers behind + builder.addListener( + forSuccessCallback { + launcher.depthController.setHasContentBehindLauncher(toState.isRecentsViewVisible) + } + ) + + handleSplitSelectionState(toState, builder, animate = true) + + setAlphas(builder, config, toState) + builder.setFloat( + recentsView, + FULLSCREEN_PROGRESS, + toState.overviewFullscreenProgress, + LINEAR, + ) + + builder.addEndListener { success: Boolean -> + if (!success && !toState.isRecentsViewVisible) { + recentsView.reset() + } + } + } + + /** + * Create or dismiss split screen select animations. + * + * @param builder if null then this will run the split select animations right away, otherwise + * will add animations to builder. + */ + private fun handleSplitSelectionState( + toState: LauncherState, + builder: PendingAnimation, + animate: Boolean, + ) { + val goingToOverviewFromWorkspaceContextual = + toState == LauncherState.OVERVIEW && launcher.isSplitSelectionActive + if ( + toState != LauncherState.OVERVIEW_SPLIT_SELECT && + !goingToOverviewFromWorkspaceContextual + ) { + // Not going to split + return + } + + // Create transition animations to split select + val orientationHandler = recentsView.pagedOrientationHandler + val taskViewsFloat = + orientationHandler.getSplitSelectTaskOffset( + TASK_PRIMARY_SPLIT_TRANSLATION, + TASK_SECONDARY_SPLIT_TRANSLATION, + launcher.deviceProfile, + ) + + val timings = + AnimUtils.getDeviceOverviewToSplitTimings( + launcher.deviceProfile.getDeviceProperties().isTablet + ) + if (!goingToOverviewFromWorkspaceContextual) { + // This animation is already done for the contextual case, don't redo it + recentsView.createSplitSelectInitAnimation( + builder, + toState.getTransitionDuration(launcher, true), + ) + } + // Shift tasks vertically downward to get out of placeholder view + builder.setFloat( + recentsView, + taskViewsFloat.first, + toState.getSplitSelectTranslation(launcher), + timings.gridSlidePrimaryInterpolator, + ) + // Zero out horizontal translation + builder.setFloat( + recentsView, + taskViewsFloat.second, + 0f, + timings.gridSlideSecondaryInterpolator, + ) + + recentsView.handleDesktopTaskInSplitSelectState( + builder, + timings.desktopTaskFadeInterpolator, + ) + + if (!animate) { + builder.buildAnim().apply { + start() + end() + } + } + } + + private fun setAlphas( + propertySetter: PropertySetter, + config: StateAnimationConfig, + state: LauncherState, + ) { + val clearAllButtonAlpha = + if (state.areElementsVisible(launcher, LauncherState.CLEAR_ALL_BUTTON)) 1f else 0f + propertySetter.setFloat( + recentsView.clearAllButton, + ClearAllButton.VISIBILITY_ALPHA, + clearAllButtonAlpha, + LINEAR, + ) + val overviewButtonAlpha = + if (state.areElementsVisible(launcher, LauncherState.OVERVIEW_ACTIONS)) 1f else 0f + propertySetter.setFloat( + launcher.actionsView.visibilityAlpha, + AnimatedFloat.VALUE, + overviewButtonAlpha, + config.getInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, LINEAR), + ) + recentsView.addDeskButton?.let { + propertySetter.setFloat( + it, + AddDesktopButton.VISIBILITY_ALPHA, + if (state.areElementsVisible(launcher, LauncherState.ADD_DESK_BUTTON)) 1f else 0f, + LINEAR, + ) + } + } + + private fun getOverviewInterpolator(fromState: LauncherState, toState: LauncherState) = + when { + fromState == LauncherState.QUICK_SWITCH_FROM_HOME -> ACCELERATE_DECELERATE + toState.isRecentsViewVisible -> INSTANT + else -> FINAL_FRAME + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt index fe04a57e9f..abe0e23432 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt +++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt @@ -26,31 +26,43 @@ import android.content.pm.ActivityInfo import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps import android.content.pm.ShortcutInfo +import android.graphics.Bitmap +import android.graphics.Rect import android.os.Build import android.os.Bundle +import android.os.Flags.allowPrivateProfile import android.os.IBinder import android.os.UserHandle import android.os.UserManager import android.util.ArrayMap +import android.view.SurfaceControlViewHost import android.widget.Toast import android.window.RemoteTransition +import android.window.ScreenCapture +import com.android.launcher3.BaseActivity import androidx.annotation.RequiresApi -import app.lawnchair.LawnchairApp import com.android.launcher3.Flags.enablePrivateSpace -import com.android.launcher3.Flags.enablePrivateSpaceInstallShortcut -import com.android.launcher3.Flags.privateSpaceAppInstallerButton import com.android.launcher3.Flags.privateSpaceSysAppsSeparation import com.android.launcher3.R import com.android.launcher3.Utilities +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppSingleton import com.android.launcher3.proxy.ProxyActivityStarter +import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController import com.android.launcher3.util.ApiWrapper import com.android.launcher3.util.Executors import com.android.launcher3.util.StartActivityParams import com.android.launcher3.util.UserIconInfo import com.android.quickstep.util.FadeOutRemoteTransition +import java.util.function.Supplier +import javax.inject.Inject + +import app.lawnchair.LawnchairApp /** A wrapper for the hidden API calls */ -open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { +@LauncherAppSingleton +open class SystemApiWrapper @Inject constructor(@ApplicationContext context: Context?) : + ApiWrapper(context) { override fun getPersons(si: ShortcutInfo) = si.persons ?: Utilities.EMPTY_PERSON_ARRAY @@ -65,7 +77,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { override fun createFadeOutAnimOptions(): ActivityOptions { return try { ActivityOptions.makeBasic().apply { - remoteTransition = RemoteTransition(FadeOutRemoteTransition()) + remoteTransition = RemoteTransition(FadeOutRemoteTransition(), "FadeOut") } } catch (t: Throwable) { super.createFadeOutAnimOptions() @@ -115,10 +127,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { override fun getAppMarketActivityIntent(packageName: String, user: UserHandle): Intent { return try { - if ( - enablePrivateSpace() && - (privateSpaceAppInstallerButton() || enablePrivateSpaceInstallShortcut()) - ) + if (allowPrivateProfile() && enablePrivateSpace()) ProxyActivityStarter.getLaunchIntent( mContext, StartActivityParams(null as PendingIntent?, 0).apply { @@ -133,7 +142,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { ) .toBundle() requireActivityResult = false - } + }, ) else super.getAppMarketActivityIntent(packageName, user) } catch (t: Throwable) { @@ -144,7 +153,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { /** Returns an intent which can be used to open Private Space Settings. */ override fun getPrivateSpaceSettingsIntent(): Intent? { return try { - if (enablePrivateSpace()) + if (allowPrivateProfile() && enablePrivateSpace()) ProxyActivityStarter.getLaunchIntent( mContext, StartActivityParams(null as PendingIntent?, 0).apply { @@ -175,6 +184,14 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { } } + override fun supportsMultiInstance(lai: LauncherActivityInfo): Boolean { + return try { + super.supportsMultiInstance(lai) || lai.supportsMultiInstance() + } catch (t: Throwable) { + false + } + } + /** * Starts an Activity which can be used to set this Launcher as the HOME app, via a consent * screen. In case the consent screen cannot be shown, or the user does not set current Launcher @@ -198,7 +215,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { allowlistToken: IBinder?, finishedReceiver: IIntentReceiver?, requiredPermission: String?, - options: Bundle? + options: Bundle?, ) { if (code != -1) { Executors.MAIN_EXECUTOR.execute { @@ -206,9 +223,9 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { context, context.getString( R.string.set_default_home_app, - context.getString(R.string.derived_app_name) + context.getString(R.string.derived_app_name), ), - Toast.LENGTH_LONG + Toast.LENGTH_LONG, ) .show() } @@ -224,4 +241,25 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { super.assignDefaultHomeRole(context) } } + + override fun createStatusBarTouchController( + launcher: BaseActivity, + isEnabledCheck: Supplier, + ): StatusBarTouchController? { + return StatusBarTouchController(launcher, isEnabledCheck) + } + + override fun isFileDrawable(shortcutInfo: ShortcutInfo) = + shortcutInfo.hasIconFile() || shortcutInfo.hasIconUri() + + override fun captureSnapshot(host: SurfaceControlViewHost, width: Int, height: Int): Bitmap = + ScreenCapture.captureLayers( + ScreenCapture.LayerCaptureArgs.Builder(host.surfacePackage!!.surfaceControl) + .setSourceCrop(Rect(0, 0, width, height)) + .setAllowProtected(true) + .setHintForSeamlessTransition(true) + .build() + ) + .asBitmap() + .copy(Bitmap.Config.ARGB_8888, true) } diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt index 4ae4577fe1..0d2cfbf028 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt @@ -17,8 +17,6 @@ package com.android.launcher3.uioverrides.flags import android.app.PendingIntent -import android.app.blob.BlobHandle.createWithSha256 -import android.app.blob.BlobStoreManager import android.content.Context import android.content.IIntentReceiver import android.content.IIntentSender.Stub @@ -29,14 +27,10 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.os.IBinder -import android.os.ParcelFileDescriptor.AutoCloseOutputStream +import android.provider.DeviceConfig import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS -import android.provider.Settings.Secure import android.text.Html import android.util.AttributeSet -import android.util.Base64 -import android.util.Base64.NO_PADDING -import android.util.Base64.NO_WRAP import android.view.inputmethod.EditorInfo import android.widget.TextView import android.widget.Toast @@ -46,47 +40,31 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceGroup import androidx.preference.PreferenceViewHolder import androidx.preference.SwitchPreference -import com.android.launcher3.AutoInstallsLayout import com.android.launcher3.ExtendedEditText import com.android.launcher3.LauncherAppState import com.android.launcher3.LauncherPrefs -import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP -import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT -import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION -import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET -import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT -import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER -import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY -import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL -import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG import com.android.launcher3.R -import com.android.launcher3.model.data.FolderInfo -import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.model.data.LauncherAppWidgetInfo -import com.android.launcher3.pm.UserCache import com.android.launcher3.proxy.ProxyActivityStarter import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher -import com.android.launcher3.shortcuts.ShortcutKey import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl import com.android.launcher3.util.Executors.MAIN_EXECUTOR -import com.android.launcher3.util.Executors.MODEL_EXECUTOR import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR -import com.android.launcher3.util.LauncherLayoutBuilder +import com.android.launcher3.util.LayoutImportExportHelper import com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_COUNT import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_DISCOVERY_TIP_COUNT import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP +import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN import com.android.launcher3.util.PluginManagerWrapper import com.android.launcher3.util.StartActivityParams -import com.android.launcher3.util.UserIconInfo import com.android.quickstep.util.DeviceConfigHelper +import com.android.quickstep.util.DeviceConfigHelper.Companion.NAMESPACE_LAUNCHER import com.android.quickstep.util.DeviceConfigHelper.DebugInfo import com.android.systemui.shared.plugins.PluginEnabler import com.android.systemui.shared.plugins.PluginPrefs -import java.io.OutputStreamWriter -import java.security.MessageDigest +import java.nio.charset.StandardCharsets import java.util.Locale import java.util.concurrent.Executor @@ -236,10 +214,15 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a ) private fun DebugInfo.getBoolValue() = - DeviceConfigHelper.prefs.getBoolean(this.key, this.valueInCode) + DeviceConfigHelper.prefs.getBoolean( + this.key, + DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, this.key, this.valueInCode), + ) private fun DebugInfo.getIntValueAsString() = - DeviceConfigHelper.prefs.getInt(this.key, this.valueInCode).toString() + DeviceConfigHelper.prefs + .getInt(this.key, DeviceConfig.getInt(NAMESPACE_LAUNCHER, this.key, this.valueInCode)) + .toString() /** * Inflates the preferences for plugins @@ -257,7 +240,7 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a val pluginPermissionApps = pm.getPackagesHoldingPermissions( arrayOf(PLUGIN_PERMISSION), - PackageManager.MATCH_DISABLED_COMPONENTS + PackageManager.MATCH_DISABLED_COMPONENTS, ) .map { it.packageName } @@ -266,7 +249,7 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a pm.queryIntentServices( Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS or - PackageManager.GET_RESOLVED_FILTER + PackageManager.GET_RESOLVED_FILTER, ) .filter { pluginPermissionApps.contains(it.serviceInfo.packageName) } } @@ -308,7 +291,7 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a infoList.forEach { manager.pluginEnabler.setDisabled( it.serviceInfo.componentName, - disabledState + disabledState, ) } manager.notifyChange(Intent(Intent.ACTION_PACKAGE_CHANGED, pluginUri)) @@ -332,6 +315,12 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a intent = Intent(launchSandboxIntent).putExtra("use_tutorial_menu", true) } ) + addPreference( + Preference(context).apply { + title = "Launch Full Gesture Tutorial" + intent = Intent(launchSandboxIntent).putExtra("use_tutorial_menu", false) + } + ) addPreference( Preference(context).apply { title = "Launch Back Tutorial" @@ -379,14 +368,15 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a addOnboardPref( "All Apps Bounce", HOME_BOUNCE_SEEN.sharedPrefKey, - HOME_BOUNCE_COUNT.sharedPrefKey + HOME_BOUNCE_COUNT.sharedPrefKey, ) addOnboardPref( "Hybrid Hotseat Education", HOTSEAT_DISCOVERY_TIP_COUNT.sharedPrefKey, - HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey + HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey, ) addOnboardPref("Taskbar Education", TASKBAR_EDU_TOOLTIP_STEP.sharedPrefKey) + addOnboardPref("Taskbar Search Education", TASKBAR_SEARCH_EDU_SEEN.sharedPrefKey) addOnboardPref("All Apps Visited Count", ALL_APPS_VISITED_COUNT.sharedPrefKey) } } @@ -414,26 +404,12 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a title = "Export" intent = createUriPickerIntent(ACTION_CREATE_DOCUMENT, MAIN_EXECUTOR) { uri -> - model.enqueueModelUpdateTask { _, dataModel, _ -> - val builder = LauncherLayoutBuilder() - dataModel.workspaceItems.forEach { info -> - val loc = - when (info.container) { - CONTAINER_DESKTOP -> - builder.atWorkspace(info.cellX, info.cellY, info.screenId) - CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId) - else -> return@forEach - } - loc.addItem(info) - } - dataModel.appWidgets.forEach { info -> - builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(info) - } - + LayoutImportExportHelper.exportModelDbAsXml(context) { layoutXml -> context.contentResolver.openOutputStream(uri).use { os -> - builder.build(OutputStreamWriter(os)) + val bytes: ByteArray = + layoutXml.toByteArray(StandardCharsets.UTF_8) // Encode to UTF-8 + os?.write(bytes) } - MAIN_EXECUTOR.execute { Toast.makeText(context, "File saved", Toast.LENGTH_LONG).show() } @@ -451,67 +427,16 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a resolver.openInputStream(uri).use { stream -> stream?.readAllBytes() ?: return@createUriPickerIntent } - - val digest = MessageDigest.getInstance("SHA-256").digest(data) - val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG) - val blobManager = context.getSystemService(BlobStoreManager::class.java)!! - - blobManager.openSession(blobManager.createSession(handle)).use { session -> - AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) } - session.allowPublicAccess() - - session.commit(ORDERED_BG_EXECUTOR) { - val key = Base64.encodeToString(digest, NO_WRAP or NO_PADDING) - Secure.putString(resolver, LAYOUT_DIGEST_KEY, key) - - MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get() - MAIN_EXECUTOR.submit { model.forceReload() }.get() - MODEL_EXECUTOR.submit {}.get() - Secure.putString(resolver, LAYOUT_DIGEST_KEY, null) - } - } + LayoutImportExportHelper.importModelFromXml(context, data) } category.addPreference(this) } } - private fun LauncherLayoutBuilder.ItemTarget.addItem(info: ItemInfo) { - val userType: String? = - when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) { - UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK - UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED - else -> null - } - when (info.itemType) { - ITEM_TYPE_APPLICATION -> - info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) } - ITEM_TYPE_DEEP_SHORTCUT -> - ShortcutKey.fromItemInfo(info).let { key -> - putShortcut(key.packageName, key.id, userType) - } - ITEM_TYPE_FOLDER -> - (info as FolderInfo).let { folderInfo -> - putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder -> - folderInfo.getContents().forEach { folderContent -> - folderBuilder.addItem(folderContent) - } - } - } - ITEM_TYPE_APPWIDGET -> - putWidget( - (info as LauncherAppWidgetInfo).providerName.packageName, - info.providerName.className, - info.spanX, - info.spanY, - userType - ) - } - } - private fun createUriPickerIntent( action: String, executor: Executor, - callback: (uri: Uri) -> Unit + callback: (uri: Uri) -> Unit, ): Intent { val pendingIntent = PendingIntent( @@ -523,7 +448,7 @@ class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, a allowlistToken: IBinder?, finishedReceiver: IIntentReceiver?, requiredPermission: String?, - options: Bundle? + options: Bundle?, ) { intent.data?.let { uri -> executor.execute { callback(uri) } } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java index 83f66b1f1f..b6e8686616 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java @@ -26,12 +26,10 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; -import androidx.core.content.ContextCompat; - -import com.android.launcher3.BuildConfigs; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.BuildConfig; +import com.android.launcher3.BuildConfigs; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.util.PluginManagerWrapper; import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.PluginListener; @@ -39,7 +37,6 @@ import com.android.systemui.shared.plugins.PluginActionManager; import com.android.systemui.shared.plugins.PluginInstance; import com.android.systemui.shared.plugins.PluginManagerImpl; import com.android.systemui.shared.plugins.PluginPrefs; -import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager; import java.io.PrintWriter; import java.util.ArrayList; @@ -47,15 +44,17 @@ import java.util.Collections; import java.util.List; import java.util.Set; -public class PluginManagerWrapperImpl extends PluginManagerWrapper { +import javax.inject.Inject; - private static final UncaughtExceptionPreHandlerManager UNCAUGHT_EXCEPTION_PRE_HANDLER_MANAGER = new UncaughtExceptionPreHandlerManager(); +@LauncherAppSingleton +public class PluginManagerWrapperImpl extends PluginManagerWrapper { private final Context mContext; private final PluginManagerImpl mPluginManager; private final PluginEnablerImpl mPluginEnabler; - public PluginManagerWrapperImpl(Context c) { + @Inject + public PluginManagerWrapperImpl(@ApplicationContext Context c) { mContext = c; mPluginEnabler = new PluginEnablerImpl(c); List privilegedPlugins = Collections.emptyList(); @@ -64,13 +63,15 @@ public class PluginManagerWrapperImpl extends PluginManagerWrapper { new PluginInstance.VersionCheckerImpl(), privilegedPlugins, BuildConfigs.IS_DEBUG_DEVICE); PluginActionManager.Factory instanceManagerFactory = new PluginActionManager.Factory( - c, c.getPackageManager(), ContextCompat.getMainExecutor(c), MODEL_EXECUTOR, + c, c.getPackageManager(), c.getMainExecutor(), MODEL_EXECUTOR, c.getSystemService(NotificationManager.class), mPluginEnabler, privilegedPlugins, instanceFactory); + // Use null preHandlerManager, as the handler is never unregistered which can cause leaks + // when using multiple dagger graphs. mPluginManager = new PluginManagerImpl(c, instanceManagerFactory, BuildConfigs.IS_DEBUG_DEVICE, - UNCAUGHT_EXCEPTION_PRE_HANDLER_MANAGER, mPluginEnabler, + null /* preHandlerManager */, mPluginEnabler, new PluginPrefs(c), privilegedPlugins); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java index 1f1fe62720..e2a624d4ca 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java @@ -20,15 +20,17 @@ import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS; import android.content.Context; +import android.graphics.Color; import com.android.internal.jank.Cuj; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.ScrimColors; import com.android.quickstep.util.BaseDepthController; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; @@ -52,8 +54,7 @@ public class AllAppsState extends LauncherState { } @Override - public int getTransitionDuration( - DEVICE_PROFILE_CONTEXT context, boolean isToState) { + public int getTransitionDuration(ActivityContext context, boolean isToState) { return isToState ? context.getDeviceProfile().allAppsOpenDuration : context.getDeviceProfile().allAppsCloseDuration; @@ -106,7 +107,7 @@ public class AllAppsState extends LauncherState { @Override public int getTitle() { - return R.string.all_apps_label; + return R.string.all_apps_list_label; } @Override @@ -122,7 +123,7 @@ public class AllAppsState extends LauncherState { @Override public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) { - if (launcher.getDeviceProfile().isTablet) { + if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) { return getWorkspaceScaleAndTranslation(launcher); } else { ScaleAndTranslation overviewScaleAndTranslation = LauncherState.OVERVIEW @@ -135,10 +136,10 @@ public class AllAppsState extends LauncherState { } @Override - protected float getDepthUnchecked( - DEVICE_PROFILE_CONTEXT context) { - if (context.getDeviceProfile().isTablet) { - return context.getDeviceProfile().bottomSheetDepth; + protected + float getDepthUnchecked(DEVICE_PROFILE_CONTEXT context) { + if (context.getDeviceProfile().shouldShowAllAppsOnSheet()) { + return context.getDeviceProfile().getBottomSheetProfile().getBottomSheetDepth(); } else { // The scrim fades in at approximately 50% of the swipe gesture. if (enableScalingRevealHomeAnimation()) { @@ -152,13 +153,18 @@ public class AllAppsState extends LauncherState { } } + @Override + public boolean shouldBlurWorkspace(LauncherState targetState) { + return targetState == ALL_APPS || targetState == NORMAL; + } + @Override public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { PageAlphaProvider superPageAlphaProvider = super.getWorkspacePageAlphaProvider(launcher); return new PageAlphaProvider(DECELERATE_2) { @Override public float getPageAlpha(int pageIndex) { - return launcher.getDeviceProfile().isTablet + return isWorkspaceVisible(launcher.getDeviceProfile()) ? superPageAlphaProvider.getPageAlpha(pageIndex) : 0; } @@ -168,13 +174,16 @@ public class AllAppsState extends LauncherState { @Override public int getVisibleElements(Launcher launcher) { int elements = ALL_APPS_CONTENT | FLOATING_SEARCH_BAR; - // Only add HOTSEAT_ICONS for tablets in ALL_APPS state. - if (launcher.getDeviceProfile().isTablet) { + if (isWorkspaceVisible(launcher.getDeviceProfile())) { elements |= HOTSEAT_ICONS; } return elements; } + private static boolean isWorkspaceVisible(DeviceProfile deviceProfile) { + return deviceProfile.getDeviceProperties().isTablet() || (Flags.allAppsSheetForHandheld() && Flags.allAppsBlur()); + } + @Override public int getFloatingSearchBarRestingMarginBottom(Launcher launcher) { return 0; @@ -195,30 +204,24 @@ public class AllAppsState extends LauncherState { @Override public boolean shouldFloatingSearchBarUsePillWhenUnfocused(Launcher launcher) { DeviceProfile dp = launcher.getDeviceProfile(); - return dp.isPhone && !dp.isLandscape; + return dp.getDeviceProperties().isPhone() && !dp.getDeviceProperties().isLandscape(); } @Override - public LauncherState getHistoryForState(LauncherState previousState) { - return previousState == BACKGROUND_APP ? QUICK_SWITCH_FROM_HOME - : previousState == OVERVIEW ? OVERVIEW : NORMAL; - } - - @Override - public float[] getOverviewScaleAndOffset(Launcher launcher) { - if (!FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()) { - return super.getOverviewScaleAndOffset(launcher); + public ScrimColors getWorkspaceScrimColor(Launcher launcher) { + int backgroundColor; + if (!launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) { + // Lawnchair-TODO-Colour: LawnchairUtilsKt.getAllAppsScrimColor(launcher) + materialColorSurfaceDim + // Always use an opaque scrim if there's no sheet. + backgroundColor = launcher.getResources().getColor(R.color.materialColorSurfaceDim); + } else if (!Flags.allAppsBlur()) { + // Lawnchair-TODO-Colour: LawnchairUtilsKt.getAllAppsScrimColor(launcher) + widgets_picker_scrim + // If there's a sheet but no blur, use the old scrim color. + backgroundColor = launcher.getResources().getColor(R.color.widgets_picker_scrim); + } else { + // Lawnchair-TODO-Colour: LawnchairUtilsKt.getAllAppsScrimColor(launcher) + allAppsScrimColor + backgroundColor = Themes.getAttrColor(launcher, R.attr.allAppsScrimColor); } - // This handles the case of returning to the previous app from Overview -> All Apps gesture. - // This is the start scale/offset of overview that will be used for that transition. - // TODO (b/283336332): Translate in Y direction (ideally with overview resistance). - return new float[] {0.5f /* scale */, NO_OFFSET}; - } - - @Override - public int getWorkspaceScrimColor(Launcher launcher) { - return launcher.getDeviceProfile().isTablet - ? launcher.getResources().getColor(android.R.color.transparent) - : LawnchairUtilsKt.getAllAppsScrimColor(launcher); + return new ScrimColors(backgroundColor, /* foregroundColor */ Color.TRANSPARENT); } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java index 262564613a..508643afd7 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java @@ -17,7 +17,6 @@ package com.android.launcher3.uioverrides.states; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import android.content.Context; import android.graphics.Color; @@ -25,6 +24,7 @@ import android.graphics.Color; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.views.ScrimColors; import com.android.quickstep.util.BaseDepthController; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.views.RecentsView; @@ -35,7 +35,8 @@ import com.android.quickstep.views.RecentsView; public class BackgroundAppState extends OverviewState { private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_RECENTS_VIEW_VISIBLE - | FLAG_WORKSPACE_INACCESSIBLE | FLAG_NON_INTERACTIVE | FLAG_CLOSE_POPUPS; + | FLAG_WORKSPACE_INACCESSIBLE | FLAG_NON_INTERACTIVE | FLAG_CLOSE_POPUPS + | FLAG_SKIP_STATE_ANNOUNCEMENT; public BackgroundAppState(int id) { this(id, LAUNCHER_STATE_BACKGROUND); @@ -51,9 +52,11 @@ public class BackgroundAppState extends OverviewState { return super.getVerticalProgress(launcher); } RecentsView recentsView = launcher.getOverviewPanel(); - int transitionLength = LayoutUtils.getShelfTrackingDistance(launcher, + int transitionLength = LayoutUtils.getShelfTrackingDistance( + launcher, launcher.getDeviceProfile(), - recentsView.getPagedOrientationHandler()); + recentsView.getPagedOrientationHandler(), + recentsView.getContainerInterface()); AllAppsTransitionController controller = launcher.getAllAppsController(); float scrollRange = Math.max(controller.getShiftRange(), 1); float progressDelta = (transitionLength / scrollRange); @@ -75,7 +78,8 @@ public class BackgroundAppState extends OverviewState { return super.getVisibleElements(launcher) & ~OVERVIEW_ACTIONS & ~CLEAR_ALL_BUTTON - & ~VERTICAL_SWIPE_INDICATOR; + & ~VERTICAL_SWIPE_INDICATOR + & ~ADD_DESK_BUTTON; } @Override @@ -88,6 +92,11 @@ public class BackgroundAppState extends OverviewState { return true; } + @Override + public boolean showExplodedDesktopView() { + return false; + } + @Override protected float getDepthUnchecked(Context context) { if (Launcher.getLauncher(context).areDesktopTasksVisible()) { @@ -101,14 +110,15 @@ public class BackgroundAppState extends OverviewState { } @Override - public int getWorkspaceScrimColor(Launcher launcher) { - return Color.TRANSPARENT; + public ScrimColors getWorkspaceScrimColor(Launcher launcher) { + return new ScrimColors( + /* backgroundColor= */ Color.TRANSPARENT, + /* foregroundColor= */ Color.TRANSPARENT); } @Override public boolean isTaskbarAlignedWithHotseat(Launcher launcher) { - if (ENABLE_SHELL_TRANSITIONS) return false; - return super.isTaskbarAlignedWithHotseat(launcher); + return false; } @Override diff --git a/src/com/android/launcher3/states/HintState.java b/quickstep/src/com/android/launcher3/uioverrides/states/HintState.java similarity index 68% rename from src/com/android/launcher3/states/HintState.java rename to quickstep/src/com/android/launcher3/uioverrides/states/HintState.java index 61a4a395e3..bfa7d26cf9 100644 --- a/src/com/android/launcher3/states/HintState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/HintState.java @@ -13,17 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.states; +package com.android.launcher3.uioverrides.states; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; import android.content.Context; +import android.graphics.Color; import androidx.core.graphics.ColorUtils; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.ScrimColors; import app.lawnchair.theme.color.tokens.ColorTokens; @@ -46,7 +49,7 @@ public class HintState extends LauncherState { } @Override - public int getTransitionDuration(Context context, boolean isToState) { + public int getTransitionDuration(ActivityContext context, boolean isToState) { return 80; } @@ -60,9 +63,16 @@ public class HintState extends LauncherState { } @Override - public int getWorkspaceScrimColor(Launcher launcher) { - return ColorUtils.setAlphaComponent( - ColorTokens.OverviewScrim.resolveColor(launcher), 100); + public ScrimColors getWorkspaceScrimColor(Launcher launcher) { + // pE-TODO(QPR1): return ColorUtils.setAlphaComponent( + // ColorTokens.OverviewScrim.resolveColor(launcher), 100); + ScrimColors overviewStateColor = OVERVIEW.getWorkspaceScrimColor(launcher); + return new ScrimColors( + /* backgroundColor */ + ColorUtils.setAlphaComponent(overviewStateColor.getBackgroundColor(), + Math.round(Color.valueOf(overviewStateColor.getBackgroundColor()).alpha() + * 100)), + /* foregroundColor */ Color.TRANSPARENT); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java index 932d241307..7348fb2eda 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java @@ -15,14 +15,14 @@ */ package com.android.launcher3.uioverrides.states; +import static com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW; -import android.content.Context; import android.graphics.Rect; -import com.android.launcher3.Flags; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.views.ActivityContext; import com.android.quickstep.views.RecentsView; /** @@ -39,17 +39,20 @@ public class OverviewModalTaskState extends OverviewState { } @Override - public int getTransitionDuration(Context launcher, boolean isToState) { + public int getTransitionDuration(ActivityContext launcher, boolean isToState) { return 300; } @Override public int getVisibleElements(Launcher launcher) { - return OVERVIEW_ACTIONS | CLEAR_ALL_BUTTON; + return OVERVIEW_ACTIONS; } @Override public float[] getOverviewScaleAndOffset(Launcher launcher) { + if (enableGridOnlyOverview()) { + return super.getOverviewScaleAndOffset(launcher); + } return getOverviewScaleAndOffsetForModalState(launcher.getOverviewPanel()); } @@ -65,7 +68,7 @@ public class OverviewModalTaskState extends OverviewState { @Override public boolean isTaskbarStashed(Launcher launcher) { - if (Flags.enableGridOnlyOverview()) { + if (enableGridOnlyOverview()) { return true; } return super.isTaskbarStashed(launcher); diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java index ea1b01b4d5..3cc9431cef 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java @@ -16,20 +16,24 @@ package com.android.launcher3.uioverrides.states; import static com.android.app.animation.Interpolators.DECELERATE_2; +import static com.android.launcher3.Flags.enableDesktopExplodedView; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW; -import static com.android.wm.shell.Flags.enableSplitContextual; import android.content.Context; import android.graphics.Rect; import android.os.SystemProperties; +import androidx.core.graphics.ColorUtils; + import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.ScrimColors; import com.android.quickstep.util.BaseDepthController; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.views.RecentsView; @@ -66,11 +70,10 @@ public class OverviewState extends LauncherState { } @Override - public int getTransitionDuration(Context context, boolean isToState) { + public int getTransitionDuration(ActivityContext context, boolean isToState) { if (isToState) { - // In gesture modes, overview comes in all the way from the side, so give it - // more time. - return DisplayController.getNavigationMode(context).hasGestures + // In gesture modes, overview comes in all the way from the side, so give it more time. + return DisplayController.getNavigationMode(context.asContext()).hasGestures ? OVERVIEW_SLIDE_IN_DURATION : OVERVIEW_POP_IN_DURATION; } else { @@ -85,9 +88,8 @@ public class OverviewState extends LauncherState { recentsView.getTaskSize(sTempRect); float scale; DeviceProfile deviceProfile = launcher.getDeviceProfile(); - if (deviceProfile.isTwoPanels) { - // In two panel layout, width does not include both panels or space between - // them, so + if (deviceProfile.getDeviceProperties().isTwoPanels()) { + // In two panel layout, width does not include both panels or space between them, so // use height instead. We do not use height for handheld, as cell layout can be // shorter than a task and we want the workspace to scale down to task size. scale = (float) sTempRect.height() / deviceProfile.getCellLayoutHeight(); @@ -100,7 +102,7 @@ public class OverviewState extends LauncherState { @Override public float[] getOverviewScaleAndOffset(Launcher launcher) { - return new float[] { NO_SCALE, NO_OFFSET }; + return new float[] {NO_SCALE, NO_OFFSET}; } @Override @@ -118,12 +120,12 @@ public class OverviewState extends LauncherState { if (PreferenceManager.getInstance(launcher).getRecentsActionClearAll().get()) { return OVERVIEW_ACTIONS; } - int elements = CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS; + int elements = CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS | ADD_DESK_BUTTON; DeviceProfile dp = launcher.getDeviceProfile(); boolean showFloatingSearch; - if (dp.isPhone) { + if (dp.getDeviceProperties().isPhone()) { // Only show search in phone overview in portrait mode. - showFloatingSearch = !dp.isLandscape; + showFloatingSearch = !dp.getDeviceProperties().isLandscape(); } else { // Only show search in tablet overview if taskbar is not visible. showFloatingSearch = !dp.isTaskbarPresent || isTaskbarStashed(launcher); @@ -131,15 +133,15 @@ public class OverviewState extends LauncherState { if (showFloatingSearch) { elements |= FLOATING_SEARCH_BAR; } - if (enableSplitContextual() && launcher.isSplitSelectionActive()) { - elements &= ~CLEAR_ALL_BUTTON; + if (launcher.isSplitSelectionActive()) { + elements &= ~CLEAR_ALL_BUTTON & ~ADD_DESK_BUTTON; } return elements; } @Override public float getSplitSelectTranslation(Launcher launcher) { - if (!enableSplitContextual() || !launcher.isSplitSelectionActive()) { + if (!launcher.isSplitSelectionActive()) { return 0f; } RecentsView recentsView = launcher.getOverviewPanel(); @@ -155,7 +157,7 @@ public class OverviewState extends LauncherState { @Override public boolean shouldFloatingSearchBarUsePillWhenUnfocused(Launcher launcher) { DeviceProfile dp = launcher.getDeviceProfile(); - return dp.isPhone && !dp.isLandscape; + return dp.getDeviceProperties().isPhone() && !dp.getDeviceProperties().isLandscape(); } @Override @@ -164,13 +166,24 @@ public class OverviewState extends LauncherState { } @Override - public int getWorkspaceScrimColor(Launcher launcher) { - return ColorTokens.OverviewScrim.resolveColor(launcher); + public ScrimColors getWorkspaceScrimColor(Launcher launcher) { + // Lawnchair-TODO-Colour: LawnchairUtilsKt.getAllAppsScrimColor(launcher) + allAppsScrimColorOverBlur + // Lawnchair-TODO-Colour: LawnchairUtilsKt.getAllAppsScrimColor(launcher) + allAppsScrimColor + return new ScrimColors( + /* backgroundColor */ Themes.getAttrColor(launcher, R.attr.overviewScrimColor), + /* foregroundColor */ ColorUtils.compositeColors( + Themes.getAttrColor(launcher, R.attr.overviewScrimForegroundPrimary), + Themes.getAttrColor(launcher, R.attr.overviewScrimForegroundSecondary))); } @Override public boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) { - return deviceProfile.isTablet; + return deviceProfile.getDeviceProperties().isTablet(); + } + + @Override + public boolean showExplodedDesktopView() { + return enableDesktopExplodedView(); } @Override @@ -215,9 +228,9 @@ public class OverviewState extends LauncherState { public void onBackInvoked(Launcher launcher) { RecentsView recentsView = launcher.getOverviewPanel(); TaskView taskView = recentsView.getRunningTaskView(); - if (taskView != null) { + if (taskView != null && !taskView.isBeingDismissed()) { if (recentsView.isTaskViewFullyVisible(taskView)) { - taskView.launchTasks(); + taskView.launchWithAnimation(); } else { recentsView.snapToPage(recentsView.indexOfChild(taskView)); } @@ -235,16 +248,14 @@ public class OverviewState extends LauncherState { } /** - * New Overview substate that represents the overview in modal mode (one task - * shown on its own) + * New Overview substate that represents the overview in modal mode (one task shown on its own) */ public static OverviewState newModalTaskState(int id) { return new OverviewModalTaskState(id); } /** - * New Overview substate representing state where 1 app for split screen has - * been selected and + * New Overview substate representing state where 1 app for split screen has been selected and * pinned and user is selecting the second one */ public static OverviewState newSplitSelectState(int id) { diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java index dfad4096bc..4058b55637 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java @@ -17,12 +17,7 @@ package com.android.launcher3.uioverrides.states; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; -import android.graphics.Color; - -import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; -import com.android.launcher3.R; -import com.android.launcher3.util.Themes; /** * State to indicate we are about to launch a recent task. Note that this state is only used when @@ -43,19 +38,6 @@ public class QuickSwitchState extends BackgroundAppState { return new ScaleAndTranslation(0.9f, 0, translationY); } - @Override - public int getWorkspaceScrimColor(Launcher launcher) { - if (launcher.areDesktopTasksVisible()) { - // No scrim while desktop tasks are visible - return Color.TRANSPARENT; - } - DeviceProfile dp = launcher.getDeviceProfile(); - if (dp.isTaskbarPresentInApps) { - return launcher.getColor(R.color.taskbar_background); - } - return Themes.getAttrColor(launcher, R.attr.overviewScrimColor); - } - @Override public float getVerticalProgress(Launcher launcher) { // Don't move all apps shelf while quick-switching (just let it fade). @@ -76,4 +58,9 @@ public class QuickSwitchState extends BackgroundAppState { public boolean isTaskbarAlignedWithHotseat(Launcher launcher) { return false; } + + @Override + public boolean detachDesktopCarousel() { + return true; + } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java index 41a9c047f4..3a6de197ac 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java @@ -78,14 +78,12 @@ public class QuickstepAtomicAnimationFactory extends private static final float RECENTS_PREPARE_SCALE = 1.33f; // Scale workspace takes before animating in private static final float WORKSPACE_PREPARE_SCALE = 0.92f; - // Constants to specify how to scroll RecentsView to the default page if it's - // not already there. + // Constants to specify how to scroll RecentsView to the default page if it's not already there. private static final int DEFAULT_PAGE = 0; private static final int PER_PAGE_SCROLL_DURATION = 150; private static final int MAX_PAGE_SCROLL_DURATION = 750; - // Due to use of physics, duration may differ between devices so we need to - // calculate and + // Due to use of physics, duration may differ between devices so we need to calculate and // cache the value. private int mHintToNormalDuration = -1; @@ -97,8 +95,10 @@ public class QuickstepAtomicAnimationFactory extends public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState, StateAnimationConfig config) { RecentsView overview = mContainer.getOverviewPanel(); + boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContainer); if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) { - overview.switchToScreenshot(() -> overview.finishRecentsAnimation(true /* toRecents */, null)); + overview.switchToScreenshot(() -> + overview.finishRecentsAnimation(true /* toRecents */, null)); if (fromState == OVERVIEW_SPLIT_SELECT) { config.setInterpolator(ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN, @@ -107,11 +107,15 @@ public class QuickstepAtomicAnimationFactory extends clampToProgress(LINEAR, 0, 0.33f)); } - // We sync the scrim fade with the taskbar animation duration to avoid any - // flickers for + // We sync the scrim fade with the taskbar animation duration to avoid any flickers for // taskbar icons disappearing before hotseat icons show up. - float scrimUpperBoundFromSplit = QuickstepTransitionManager.getTaskbarToHomeDuration() - / (float) config.duration; + boolean isPinnedTaskbarAndNotInDesktopMode = + isPinnedTaskbar && !DisplayController.isInDesktopMode(mContainer); + float scrimUpperBoundFromSplit = + QuickstepTransitionManager.getTaskbarToHomeDuration( + isPinnedTaskbarAndNotInDesktopMode) + / (float) config.duration; + scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f); config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f)); config.setInterpolator(ANIM_SCRIM_FADE, fromState == OVERVIEW_SPLIT_SELECT @@ -121,7 +125,7 @@ public class QuickstepAtomicAnimationFactory extends config.setInterpolator(ANIM_WORKSPACE_FADE, ACCELERATE); if (DisplayController.getNavigationMode(mContainer).hasGestures - && overview.getTaskViewCount() > 0) { + && overview.hasTaskViews()) { // Overview is going offscreen, so keep it at its current scale and opacity. config.setInterpolator(ANIM_OVERVIEW_SCALE, FINAL_FRAME); config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME); @@ -137,11 +141,12 @@ public class QuickstepAtomicAnimationFactory extends numPagesToScroll * PER_PAGE_SCROLL_DURATION); config.duration = Math.max(config.duration, scrollDuration); - // Sync scroll so that it ends before or at the same time as the taskbar - // animation. + // Sync scroll so that it ends before or at the same time as the taskbar animation. if (mContainer.getDeviceProfile().isTaskbarPresent) { config.duration = Math.min( - config.duration, QuickstepTransitionManager.getTaskbarToHomeDuration()); + config.duration, + QuickstepTransitionManager.getTaskbarToHomeDuration( + isPinnedTaskbarAndNotInDesktopMode)); } overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration)); } else { @@ -151,8 +156,7 @@ public class QuickstepAtomicAnimationFactory extends } Workspace workspace = mContainer.getWorkspace(); - // Start from a higher workspace scale, but only if we're invisible so we don't - // jump. + // Start from a higher workspace scale, but only if we're invisible so we don't jump. boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE; if (isWorkspaceVisible) { CellLayout currentChild = (CellLayout) workspace.getChildAt( @@ -178,7 +182,7 @@ public class QuickstepAtomicAnimationFactory extends config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCELERATE); // Scrolling in tasks, so show straight away - if (overview.getTaskViewCount() > 0) { + if (overview.hasTaskViews()) { config.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT); } else { config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2); @@ -216,7 +220,7 @@ public class QuickstepAtomicAnimationFactory extends } else if (fromState == NORMAL && toState == ALL_APPS) { AllAppsSwipeController.applyNormalToAllAppsAnimConfig(mContainer, config); } else if (fromState == OVERVIEW && toState == OVERVIEW_SPLIT_SELECT) { - SplitAnimationTimings timings = mContainer.getDeviceProfile().isTablet + SplitAnimationTimings timings = mContainer.getDeviceProfile().getDeviceProperties().isTablet() ? SplitAnimationTimings.TABLET_OVERVIEW_TO_SPLIT : SplitAnimationTimings.PHONE_OVERVIEW_TO_SPLIT; config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java index 3ae221bbeb..c2ec17ab3f 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java @@ -16,9 +16,8 @@ package com.android.launcher3.uioverrides.states; -import android.content.Context; - import com.android.launcher3.Launcher; +import com.android.launcher3.views.ActivityContext; import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.views.RecentsView; @@ -43,11 +42,10 @@ public class SplitScreenSelectState extends OverviewState { } @Override - public int getTransitionDuration(Context context, boolean isToState) { - boolean isTablet = ((Launcher) context).getDeviceProfile().isTablet; - if (isToState && isTablet) { + public int getTransitionDuration(ActivityContext context, boolean isToState) { + if (isToState && context.getDeviceProfile().getDeviceProperties().isTablet()) { return SplitAnimationTimings.TABLET_ENTER_DURATION; - } else if (isToState && !isTablet) { + } else if (isToState) { return SplitAnimationTimings.PHONE_ENTER_DURATION; } else { return SplitAnimationTimings.ABORT_DURATION; diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java index 11e0ed528c..dce158f63f 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java @@ -28,6 +28,7 @@ import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE; import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS; +import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import android.animation.AnimatorSet; @@ -44,15 +45,16 @@ import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.TouchController; +import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.OverviewToHomeAnim; import com.android.quickstep.views.RecentsView; +import com.android.systemui.contextualeducation.GestureType; import java.util.function.BiConsumer; @@ -134,7 +136,7 @@ public class NavBarToHomeTouchController implements TouchController, } private float getShiftRange() { - return mLauncher.getDeviceProfile().heightPx; + return mLauncher.getDeviceProfile().getDeviceProperties().getHeightPx(); } @Override @@ -155,10 +157,16 @@ public class NavBarToHomeTouchController implements TouchController, AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_TASK_MENU); } else if (mStartState == ALL_APPS) { AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); - builder.setFloat(allAppsController, ALL_APPS_PULL_BACK_TRANSLATION, - -mPullbackDistance, PULLBACK_INTERPOLATOR); - builder.setFloat(allAppsController, ALL_APPS_PULL_BACK_ALPHA, - 0.5f, PULLBACK_INTERPOLATOR); + if (mLauncher.getDeviceProfile().shouldShowAllAppsOnSheet()) { + allAppsController.setShouldScaleHeader(true); + builder.addAnimatedFloat(allAppsController.getAllAppScale(), 1f, + PREDICTIVE_BACK_MIN_SCALE, PULLBACK_INTERPOLATOR); + } else { + builder.setFloat(allAppsController, ALL_APPS_PULL_BACK_TRANSLATION, + -mPullbackDistance, PULLBACK_INTERPOLATOR); + builder.setFloat(allAppsController, ALL_APPS_PULL_BACK_ALPHA, + 0.5f, PULLBACK_INTERPOLATOR); + } } AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mLauncher); if (topView != null) { @@ -208,10 +216,7 @@ public class NavBarToHomeTouchController implements TouchController, mLauncher.getStateManager().addStateListener(listener); onSwipeInteractionCompleted(mEndState); }; - new OverviewToHomeAnim(mLauncher, onReachedHome, - FeatureFlags.enableSplitContextually() - ? mCancelSplitRunnable - : null) + new OverviewToHomeAnim(mLauncher, onReachedHome, mCancelSplitRunnable) .animateWithVelocity(velocity); } else { mLauncher.getStateManager().goToState(mEndState, true, @@ -219,6 +224,8 @@ public class NavBarToHomeTouchController implements TouchController, } if (mStartState != mEndState) { logHomeGesture(); + SystemUiProxy.INSTANCE.get(mLauncher).updateContextualEduStats( + mSwipeDetector.isTrackpadGesture(), GestureType.HOME); } AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(mLauncher); if (topOpenView != null) { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java index 3325009b71..8d907f4183 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java @@ -17,7 +17,7 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; -import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; +import static com.android.launcher3.LauncherAnimUtils.SCRIM_COLORS; import static com.android.launcher3.LauncherAnimUtils.newSingleUseCancelListener; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.HINT_STATE; @@ -39,16 +39,15 @@ import android.view.MotionEvent; import android.view.ViewConfiguration; import com.android.internal.jank.Cuj; -import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.VibratorWrapper; +import com.android.launcher3.views.ScrimColorsEvaluator; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.AnimatorControllerWithResistance; import com.android.quickstep.util.MotionPauseDetector; @@ -64,6 +63,8 @@ import java.util.function.BiConsumer; * first home screen instead of to Overview. */ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouchController { + + public static final String TAG = "NoButtonNavbarToOverviewTouchController"; private static final float ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER = 2.5f; // How much of the movement to use for translating overview after swipe and hold. @@ -86,15 +87,19 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch private AnimatorPlaybackController mOverviewResistYAnim; // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator. - private ObjectAnimator mNormalToHintOverviewScrimAnimator; + private ValueAnimator mNormalToHintOverviewScrimAnimator; + + private final QuickstepLauncher mLauncher; + private boolean mIsTrackpadSwipe; /** * @param cancelSplitRunnable Called when split placeholder view needs to be cancelled. * Animation should be added to the provided AnimatorSet */ - public NoButtonNavbarToOverviewTouchController(Launcher l, + public NoButtonNavbarToOverviewTouchController(QuickstepLauncher l, BiConsumer cancelSplitRunnable) { super(l); + mLauncher = l; mRecentsView = l.getOverviewPanel(); mMotionPauseDetector = new MotionPauseDetector(l); mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop(); @@ -104,7 +109,9 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch @Override protected boolean canInterceptTouch(MotionEvent ev) { - if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher) + mIsTrackpadSwipe = isTrackpadMotionEvent(ev); + mLauncher.setCanShowAllAppsEducationView(!mIsTrackpadSwipe); + if (!mIsTrackpadSwipe && DisplayController.getNavigationMode(mLauncher) == THREE_BUTTONS) { return false; } @@ -130,7 +137,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch float progressMultiplier = super.initCurrentAnimation(); if (mToState == HINT_STATE) { // Track the drag across the entire height of the screen. - progressMultiplier = -1f / mLauncher.getDeviceProfile().heightPx; + progressMultiplier = -1f / mLauncher.getDeviceProfile().getDeviceProperties().getHeightPx(); } return progressMultiplier; } @@ -148,6 +155,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch super.onDragStart(start, startDisplacement); mMotionPauseDetector.clear(); + mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe); if (handlingOverviewAnim()) { InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS, @@ -156,9 +164,10 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch } if (mFromState == NORMAL && mToState == HINT_STATE) { - mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofArgb( + mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofObject( mLauncher.getScrimView(), - VIEW_BACKGROUND_COLOR, + SCRIM_COLORS, + ScrimColorsEvaluator.INSTANCE, mFromState.getWorkspaceScrimColor(mLauncher), mToState.getWorkspaceScrimColor(mLauncher)); } @@ -167,6 +176,11 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch mOverviewResistYAnim = null; } + @Override + public String dump() { + return TAG; + } + @Override protected void updateProgress(float fraction) { super.updateProgress(fraction); @@ -191,6 +205,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch } mMotionPauseDetector.clear(); + mIsTrackpadSwipe = false; mNormalToHintOverviewScrimAnimator = null; if (mLauncher.isInState(OVERVIEW)) { // Normally we would cleanup the state based on mCurrentAnimation, but since we stop @@ -214,11 +229,6 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch mCancelSplitRunnable.accept(animatorSet, duration); animatorSet.start(); } - if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && - ((mFromState == NORMAL && mToState == ALL_APPS) - || (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) { - mVibratorWrapper.vibrateForDragBump(); - } } private void onMotionPauseDetected() { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java index 20e9960e57..416c8ab4fd 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java @@ -16,7 +16,6 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static android.view.MotionEvent.ACTION_DOWN; -import static android.view.MotionEvent.ACTION_MOVE; import static com.android.app.animation.Interpolators.ACCELERATE_0_75; import static com.android.app.animation.Interpolators.DECELERATE_3; @@ -52,6 +51,7 @@ import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; +import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS; import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; @@ -65,6 +65,7 @@ import android.animation.ValueAnimator; import android.graphics.PointF; import android.view.MotionEvent; import android.view.animation.Interpolator; +import android.window.DesktopModeFlags; import com.android.internal.jank.Cuj; import com.android.launcher3.LauncherState; @@ -87,12 +88,11 @@ import com.android.quickstep.util.MotionPauseDetector; import com.android.quickstep.util.WorkspaceRevealAnim; import com.android.quickstep.views.RecentsView; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; /** - * Handles quick switching to a recent task from the home screen. To give as - * much flexibility to - * the user as possible, also handles swipe up and hold to go to overview and - * swiping back home. + * Handles quick switching to a recent task from the home screen. To give as much flexibility to + * the user as possible, also handles swipe up and hold to go to overview and swiping back home. */ public class NoButtonQuickSwitchTouchController implements TouchController, BothAxesSwipeDetector.Listener { @@ -115,26 +115,28 @@ public class NoButtonQuickSwitchTouchController implements TouchController, newCancelListener(this::clearState, /* isSingleUse = */ false); private boolean mNoIntercept; - private Boolean mIsTrackpadFourFingerSwipe; private LauncherState mStartState; private boolean mIsHomeScreenVisible = true; - // As we drag, we control 3 animations: one to get non-overview components out - // of the way, + // As we drag, we control 3 animations: one to get non-overview components out of the way, // and the other two to set overview properties based on x and y progress. private AnimatorPlaybackController mNonOverviewAnim; private AnimatorPlaybackController mXOverviewAnim; private AnimatedFloat mYOverviewAnim; + private boolean mIsTrackpadSwipe; public NoButtonQuickSwitchTouchController(QuickstepLauncher launcher) { mLauncher = launcher; mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this); mRecentsView = mLauncher.getOverviewPanel(); - mXRange = mLauncher.getDeviceProfile().widthPx / 2f; + mXRange = mLauncher.getDeviceProfile().getDeviceProperties().getWidthPx() / 2f; mYRange = LayoutUtils.getShelfTrackingDistance( - mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler()); - mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange; + mLauncher, + mLauncher.getDeviceProfile(), + mRecentsView.getPagedOrientationHandler(), + mRecentsView.getContainerInterface()); + mMaxYProgress = mLauncher.getDeviceProfile().getDeviceProperties().getHeightPx() / mYRange; mMotionPauseDetector = new MotionPauseDetector(mLauncher); mMotionPauseMinDisplacement = mLauncher.getResources().getDimension( R.dimen.motion_pause_detector_min_displacement_from_app); @@ -148,17 +150,9 @@ public class NoButtonQuickSwitchTouchController implements TouchController, return false; } - // Only detect horizontal swipe for intercept, then we will allow swipe up as - // well. + // Only detect horizontal swipe for intercept, then we will allow swipe up as well. mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT, false /* ignoreSlopWhenSettling */); - } else if (isTrackpadMultiFingerSwipe(ev) && mIsTrackpadFourFingerSwipe == null - && ev.getActionMasked() == ACTION_MOVE) { - mIsTrackpadFourFingerSwipe = isTrackpadFourFingerSwipe(ev); - mNoIntercept = !mIsTrackpadFourFingerSwipe; - if (mNoIntercept) { - return false; - } } if (mNoIntercept) { @@ -175,7 +169,8 @@ public class NoButtonQuickSwitchTouchController implements TouchController, } private boolean canInterceptTouch(MotionEvent ev) { - if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher) == THREE_BUTTONS) { + if (!isTrackpadMotionEvent(ev) && DisplayController.getNavigationMode(mLauncher) + == THREE_BUTTONS) { return false; } if (!mLauncher.isInState(LauncherState.NORMAL)) { @@ -189,7 +184,14 @@ public class NoButtonQuickSwitchTouchController implements TouchController, return false; } if (isTrackpadMultiFingerSwipe(ev)) { - return isTrackpadFourFingerSwipe(ev); + mIsTrackpadSwipe = isTrackpadFourFingerSwipe(ev); + return mIsTrackpadSwipe; + } + if (DesktopModeStatus.canEnterDesktopMode(mLauncher) + //TODO(b/345296916): replace with dev option once in teamfood + && DesktopModeFlags.ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX.isTrue() + && mRecentsView.getNonDesktopTaskViewCount() < 1) { + return false; } return true; } @@ -197,6 +199,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, @Override public void onDragStart(boolean start) { mMotionPauseDetector.clear(); + mMotionPauseDetector.setIsTrackpadGesture(mIsTrackpadSwipe); if (start) { InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH); InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS, @@ -229,7 +232,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, updateNonOverviewAnim(QUICK_SWITCH_FROM_HOME, nonOverviewBuilder); mNonOverviewAnim.dispatchOnStart(); - if (mRecentsView.getTaskViewCount() == 0) { + if (!mRecentsView.hasTaskViews()) { mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> { if (!isEmpty && mSwipeDetector.isDraggingState()) { // We have loaded tasks, update the animators to start at the correct scale etc. @@ -258,32 +261,35 @@ public class NoButtonQuickSwitchTouchController implements TouchController, RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]); ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, 1f); TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, fromState.showTaskThumbnailSplash() ? 1f : 0); + DESKTOP_CAROUSEL_DETACH_PROGRESS.set(mRecentsView, + fromState.detachDesktopCarousel() ? 1f : 0); mRecentsView.setContentAlpha(1); mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress()); mLauncher.getActionsView().getVisibilityAlpha().updateValue( (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f); - mRecentsView.setTaskIconScaledDown(true); + mRecentsView.setTaskIconVisible(false); float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher); // As we drag right, animate the following properties: - // - RecentsView translationX - // - OverviewScrim - // - RecentsView fade (if it's empty) + // - RecentsView translationX + // - OverviewScrim + // - RecentsView fade (if it's empty) PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2)); xAnim.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1], LINEAR); // Use QuickSwitchState instead of OverviewState to determine scrim color, // since we need to take potential taskbar into account. - xAnim.setViewBackgroundColor(mLauncher.getScrimView(), - QUICK_SWITCH_FROM_HOME.getWorkspaceScrimColor(mLauncher), LINEAR); - if (mRecentsView.getTaskViewCount() == 0) { + xAnim.setScrimColors(mLauncher.getScrimView(), + QUICK_SWITCH_FROM_HOME.getWorkspaceScrimColor(mLauncher), + LINEAR); + if (!mRecentsView.hasTaskViews()) { xAnim.addFloat(mRecentsView, CONTENT_ALPHA, 0f, 1f, LINEAR); } mXOverviewAnim = xAnim.createPlaybackController(); mXOverviewAnim.dispatchOnStart(); // As we drag up, animate the following properties: - // - RecentsView scale - // - RecentsView fullscreenProgress + // - RecentsView scale + // - RecentsView fullscreenProgress PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2)); yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR); @@ -313,7 +319,8 @@ public class NoButtonQuickSwitchTouchController implements TouchController, if (wasHomeScreenVisible && mNonOverviewAnim != null) { mNonOverviewAnim.setPlayFraction(xProgress); } - mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress) <= 1 - ALPHA_CUTOFF_THRESHOLD; + mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress) + <= 1 - ALPHA_CUTOFF_THRESHOLD; mMotionPauseDetector.setDisallowPause(-displacement.y < mMotionPauseMinDisplacement); mMotionPauseDetector.addPosition(ev); @@ -345,13 +352,12 @@ public class NoButtonQuickSwitchTouchController implements TouchController, public void onAnimationEnd(Animator animation) { onAnimationToStateCompleted(OVERVIEW); // Animate the icon after onAnimationToStateCompleted() so it doesn't clobber. - mRecentsView.animateUpTaskIconScale(); + mRecentsView.startIconFadeInOnGestureComplete(); } }); overviewAnim.start(); - // Create an empty state transition so StateListeners get - // onStateTransitionStart(). + // Create an empty state transition so StateListeners get onStateTransitionStart(). mLauncher.getStateManager().createAnimationToNewWorkspace( OVERVIEW, config.duration, StateAnimationConfig.SKIP_ALL_ANIMATIONS) .dispatchOnStart(); @@ -373,8 +379,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, // Flinging up and right could go either home or to quick switch. // Determine the target based on the higher velocity. targetState = Math.abs(velocity.x) > Math.abs(velocity.y) - ? QUICK_SWITCH_FROM_HOME - : NORMAL; + ? QUICK_SWITCH_FROM_HOME : NORMAL; } } } else if (horizontalFling) { @@ -386,8 +391,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController, boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f; boolean passedVerticalThreshold = mYOverviewAnim.value > 1f; targetState = passedHorizontalThreshold && !passedVerticalThreshold - ? QUICK_SWITCH_FROM_HOME - : NORMAL; + ? QUICK_SWITCH_FROM_HOME : NORMAL; } // Animate the various components to the target state. diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java index 4df0f63116..68940dc65c 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java @@ -48,9 +48,9 @@ public final class PortraitOverviewStateTouchHelper { * @return true if we should intercept the motion event */ boolean canInterceptTouch(MotionEvent ev) { - if (mRecentsView.getTaskViewCount() > 0) { + if (mRecentsView.hasTaskViews()) { // Allow swiping up in the gap between the hotseat and overview. - return ev.getY() >= mRecentsView.getTaskViewAt(0).getBottom(); + return ev.getY() >= mRecentsView.getFirstTaskView().getBottom(); } else { // If there are no tasks, we only intercept if we're below the hotseat height. return isTouchOverHotseat(mLauncher, ev); diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java index b5914a1880..22055821fa 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java @@ -26,18 +26,20 @@ import android.view.MotionEvent; import com.android.app.animation.Interpolators; import com.android.internal.jank.Cuj; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.allapps.AllAppsTransitionController; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.touch.AbstractStateChangeTouchController; import com.android.launcher3.touch.AllAppsSwipeController; import com.android.launcher3.touch.SingleAxisSwipeDetector; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.uioverrides.states.OverviewState; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.views.RecentsView; +import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; /** @@ -93,9 +95,7 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr @Override protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { if (fromState == ALL_APPS && !isDragTowardPositive) { - return FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get() - ? mLauncher.getStateManager().getLastState() - : NORMAL; + return NORMAL; } else if (fromState == NORMAL && shouldOpenAllApps(isDragTowardPositive)) { return ALL_APPS; } @@ -145,8 +145,11 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr .createPlaybackController(); mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); RecentsView recentsView = mLauncher.getOverviewPanel(); - totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher, - mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler()); + totalShift = LayoutUtils.getShelfTrackingDistance( + mLauncher, + mLauncher.getDeviceProfile(), + recentsView.getPagedOrientationHandler(), + recentsView.getContainerInterface()); } else { mCurrentAnimation = mLauncher.getStateManager() .createAnimationToNewWorkspace(mToState, config); @@ -163,8 +166,19 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr @Override protected void onSwipeInteractionCompleted(LauncherState targetState) { super.onSwipeInteractionCompleted(targetState); + SystemUiProxy sysUIProxy = SystemUiProxy.INSTANCE.get(mLauncher); if (mStartState == NORMAL && targetState == OVERVIEW) { - SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG); + sysUIProxy.onOverviewShown(true, TAG); + } + + if (targetState == OVERVIEW) { + sysUIProxy.updateContextualEduStats( + mDetector.isTrackpadGesture(), GestureType.OVERVIEW); + } else if (targetState == ALL_APPS && !mDetector.isTrackpadGesture()) { + // Only update if it is touch gesture as trackpad gesture is not relevant for all apps + // which only provides keyboard education. + sysUIProxy.updateContextualEduStats( + /* isTrackpadGesture= */ false, GestureType.ALL_APPS); } } @@ -204,6 +218,11 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr @Override protected void onReinitToState(LauncherState newToState) { super.onReinitToState(newToState); + if (Flags.allAppsBlur() && mLauncher.isAllAppsBackgroundBlurEnabled() + && newToState == ALL_APPS) { + // About to start blurring during swipe to All Apps; prepare the renderer. + ((QuickstepLauncher) mLauncher).getDepthController().setEarlyWakeup(true); + } if (newToState != ALL_APPS) { InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java index 93e4fbdc00..14896e53c7 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java @@ -149,9 +149,9 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll mOverviewPanel.setFullscreenProgress(progress); if (progress > UPDATE_SYSUI_FLAGS_THRESHOLD) { int sysuiFlags = 0; - TaskView tv = mOverviewPanel.getTaskViewAt(0); - if (tv != null) { - sysuiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags(); + TaskView firstTaskView = mOverviewPanel.getFirstTaskView(); + if (firstTaskView != null) { + sysuiFlags = firstTaskView.getSysUiStatusNavFlags(); } mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags); } else { @@ -161,6 +161,6 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll @Override protected float getShiftRange() { - return mLauncher.getDeviceProfile().widthPx / 2f; + return mLauncher.getDeviceProfile().getDeviceProperties().getWidthPx() / 2f; } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java index 3d22c4835d..9d501ed116 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * Modifications copyright 2021, Lawnchair + * Modifications copyright 2025, Lawnchair */ package com.android.launcher3.uioverrides.touchcontrollers; @@ -24,102 +24,69 @@ import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll; +import static com.android.launcher3.Utilities.shouldEnableMouseInteractionChanges; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN; -import android.annotation.SuppressLint; import android.graphics.PointF; import android.util.SparseArray; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManager; import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.util.TouchController; -import com.android.launcher3.util.VibratorWrapper; import com.android.quickstep.SystemUiProxy; -import java.io.PrintWriter; +import java.util.function.Supplier; import java.lang.reflect.InvocationTargetException; import app.lawnchair.LawnchairAppKt; import app.lawnchair.util.CompatibilityKt; /** - * TouchController for handling touch events that get sent to the StatusBar. - * Once the - * Once the event delta mDownY passes the touch slop, the events start getting - * forwarded. + * TouchController for handling touch events that get sent to the StatusBar. Once the + * Once the event delta mDownY passes the touch slop, the events start getting forwarded. * All events are offset by initial Y value of the pointer. */ public class StatusBarTouchController implements TouchController { private static final String TAG = "StatusBarController"; - private final Launcher mLauncher; + private final BaseActivity mLauncher; private final SystemUiProxy mSystemUiProxy; private final float mTouchSlop; private int mLastAction; private final SparseArray mDownEvents; + private final Supplier mIsEnabledCheck; - /* - * If {@code false}, this controller should not handle the input {@link - * MotionEvent}. - */ + /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/ private boolean mCanIntercept; - private boolean mExpanded; - private boolean mVibrated; - private boolean mIsTrackpadReverseScroll; - - public StatusBarTouchController(Launcher l) { + public StatusBarTouchController(BaseActivity l, Supplier isEnabledCheck) { mLauncher = l; mSystemUiProxy = SystemUiProxy.INSTANCE.get(mLauncher); // Guard against TAPs by increasing the touch slop. mTouchSlop = 2 * ViewConfiguration.get(l).getScaledTouchSlop(); mDownEvents = new SparseArray<>(); + mIsEnabledCheck = isEnabledCheck; } @Override - public void dump(String prefix, PrintWriter writer) { - writer.println(prefix + "mCanIntercept:" + mCanIntercept); - writer.println(prefix + "mLastAction:" + MotionEvent.actionToString(mLastAction)); - writer.println(prefix + "mSysUiProxy available:" - + SystemUiProxy.INSTANCE.get(mLauncher).isActive()); + public String dump() { + return "mCanIntercept:" + mCanIntercept + + " , mLastAction:" + MotionEvent.actionToString(mLastAction) + + " , mSysUiProxy available:" + SystemUiProxy.INSTANCE.get(mLauncher).isActive(); } private void dispatchTouchEvent(MotionEvent ev) { if (mSystemUiProxy.isActive()) { mLastAction = ev.getActionMasked(); mSystemUiProxy.onStatusBarTouchEvent(ev); - } else if (!mExpanded) { - mExpanded = true; - expand(); - } - if (!mVibrated) { - mVibrated = true; - vibrate(); - } - } - - @SuppressLint({"WrongConstant", "PrivateApi"}) - private void expand() { - try { - Class.forName("android.app.StatusBarManager") - .getMethod("expandNotificationsPanel") - .invoke(mLauncher.getSystemService("statusbar")); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) { - e.printStackTrace(); - } - } - - private void vibrate() { - if (!LawnchairAppKt.getLawnchairApp(mLauncher).isVibrateOnIconAnimation()) { - VibratorWrapper.INSTANCE.get(mLauncher).vibrate(VibratorWrapper.OVERVIEW_HAPTIC); } } @@ -134,11 +101,7 @@ public class StatusBarTouchController implements TouchController { return false; } mDownEvents.clear(); - mExpanded = false; - mVibrated = false; mDownEvents.put(pid, new PointF(ev.getX(), ev.getY())); - mIsTrackpadReverseScroll = !mLauncher.isNaturalScrollingEnabled() - && isTrackpadScroll(ev); } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { // Check!! should only set it only when threshold is not entered. mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx))); @@ -152,9 +115,6 @@ public class StatusBarTouchController implements TouchController { if (action == ACTION_MOVE && mDownEvents.contains(pid)) { float dy = ev.getY(idx) - mDownEvents.get(pid).y; float dx = ev.getX(idx) - mDownEvents.get(pid).x; - if (mIsTrackpadReverseScroll) { - dy = -dy; - } // Currently input dispatcher will not do touch transfer if there are more than // one touch pointer. Hence, even if slope passed, only set the slippery flag // when there is single touch event. (context: InputDispatcher.cpp line 1445) @@ -179,7 +139,6 @@ public class StatusBarTouchController implements TouchController { mLauncher.getStatsLogManager().logger() .log(LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN); setWindowSlippery(false); - mIsTrackpadReverseScroll = false; return true; } else if (CompatibilityKt.isOnePlusStock() && action == ACTION_MOVE) { dispatchTouchEvent(ev); @@ -208,9 +167,11 @@ public class StatusBarTouchController implements TouchController { } private boolean canInterceptTouch(MotionEvent ev) { - if (!mLauncher.isInState(LauncherState.NORMAL) || - AbstractFloatingView.getTopOpenViewWithType(mLauncher, - AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) { + if (isTrackpadScroll(ev) || !mIsEnabledCheck.get() + || AbstractFloatingView.getTopOpenViewWithType(mLauncher, + AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null || ( + shouldEnableMouseInteractionChanges(mLauncher.asContext()) + && ev.getSource() == InputDevice.SOURCE_MOUSE)) { return false; } else { // For NORMAL state, only listen if the event originated above the navbar height @@ -219,6 +180,6 @@ public class StatusBarTouchController implements TouchController { return false; } } - return true; + return SystemUiProxy.INSTANCE.get(mLauncher).isActive(); } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt new file mode 100644 index 0000000000..a85f706f69 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides.touchcontrollers + +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import androidx.dynamicanimation.animation.SpringAnimation +import com.android.app.animation.Interpolators.DECELERATE +import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.R +import com.android.launcher3.Utilities.EDGE_NAV_BAR +import com.android.launcher3.Utilities.boundToRange +import com.android.launcher3.Utilities.debugLog +import com.android.launcher3.Utilities.isRtl +import com.android.launcher3.Utilities.mapToRange +import com.android.launcher3.statemanager.BaseState +import com.android.launcher3.statemanager.StateManager.StateListener +import com.android.launcher3.statemanager.StatefulContainer +import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.util.MSDLPlayerWrapper +import com.android.launcher3.util.TouchController +import com.android.mechanics.spec.Breakpoint +import com.android.mechanics.spec.Breakpoint.Companion.maxLimit +import com.android.mechanics.spec.Breakpoint.Companion.minLimit +import com.android.mechanics.spec.BreakpointKey +import com.android.mechanics.spec.DirectionalMotionSpec +import com.android.mechanics.spec.Guarantee +import com.android.mechanics.spec.InputDirection +import com.android.mechanics.spec.Mapping +import com.android.mechanics.spec.MotionSpec +import com.android.mechanics.spring.SpringParameters +import com.android.mechanics.view.DistanceGestureContext +import com.android.mechanics.view.ViewMotionValue +import com.android.quickstep.views.RecentsDismissUtils +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskView +import com.google.android.msdl.data.model.MSDLToken +import kotlin.math.abs +import kotlin.math.ceil + +/** Touch controller for handling task view card dismiss swipes */ +class TaskViewDismissTouchController>( + private val container: CONTAINER, + private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext, +) : TouchController, SingleAxisSwipeDetector.Listener where +CONTAINER : Context, +CONTAINER : RecentsViewContainer, +CONTAINER : StatefulContainer { + private val recentsView: RecentsView<*, *> = container.getOverviewPanel() + private val detector: SingleAxisSwipeDetector = + SingleAxisSwipeDetector( + container as Context, + this, + recentsView.pagedOrientationHandler.upDownSwipeDirection, + ) + private val isRtl = isRtl(container.resources) + private val upDirection: Int = recentsView.pagedOrientationHandler.getUpDirection(isRtl) + private val maxUndershoot = + container.resources.getDimension(R.dimen.task_dismiss_max_undershoot) + private val detachThreshold = + container.resources.getDimension(R.dimen.task_dismiss_detach_threshold) + private val stateListener = + object : StateListener { + override fun onStateTransitionStart(toState: T) { + springAnimation?.cancel() + clearState() + } + } + private val tempTaskThumbnailBounds = Rect() + + private var taskBeingDragged: TaskView? = null + private var taskDragDisplacementValue: ViewMotionValue? = null + private var springAnimation: RecentsDismissUtils.SpringSet? = null + private var dismissLength: Int = 0 + private var verticalFactor: Int = 0 + private var hasDismissThresholdHapticRun = false + private var initialDisplacement: Float = 0f + private var recentsScaleAnimation: SpringAnimation? = null + private var canInterceptTouch = false + private var isDismissing = false + + init { + container.getStateManager().addStateListener(stateListener) + } + + override fun onTouchControllerDestroyed() { + container.getStateManager().removeStateListener(stateListener) + } + + private fun canInterceptTouch(ev: MotionEvent): Boolean = + when { + // Don't intercept swipes on the nav bar, as user might be trying to go home during a + // task dismiss animation. + (ev.edgeFlags and EDGE_NAV_BAR) != 0 -> { + debugLog(TAG, "Not intercepting edge swipe on nav bar.") + false + } + + // Floating views that a TouchController should not try to intercept touches from. + AbstractFloatingView.getTopOpenViewWithType( + container, + AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT, + ) != null -> { + debugLog(TAG, "Not intercepting, open floating view blocking touch.") + false + } + + // Disable swiping if the task overlay is modal. + taskViewRecentsTouchContext.isRecentsModal -> { + debugLog(TAG, "Not intercepting touch in modal overlay.") + false + } + + // Do not allow dismiss while recents is scrolling. + !recentsView.scroller.isFinished -> { + debugLog(TAG, "Not intercepting touch, recents scrolling.") + false + } + + else -> + taskViewRecentsTouchContext.isRecentsInteractive.also { isRecentsInteractive -> + if (!isRecentsInteractive) { + debugLog(TAG, "Not intercepting touch, recents not interactive.") + } + } + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + // On consecutive events, end animation early so user can dismiss next task. + springAnimation?.speedUpSpringsToEnd() + + if ((ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL)) { + clearState() + } + if (ev.action == MotionEvent.ACTION_DOWN) { + canInterceptTouch = onActionDown(ev) + if (!canInterceptTouch) { + return false + } + } + // Ignore other actions if touch intercepting has not been enabled in an ACTION_DOWN event. + if (!canInterceptTouch) { + return false + } + onControllerTouchEvent(ev) + val upDirectionIsPositive = upDirection == SingleAxisSwipeDetector.DIRECTION_POSITIVE + val wasInitialTouchUp = + (upDirectionIsPositive && detector.wasInitialTouchPositive()) || + (!upDirectionIsPositive && !detector.wasInitialTouchPositive()) + return detector.isDraggingState && wasInitialTouchUp + } + + override fun onControllerTouchEvent(ev: MotionEvent?): Boolean = detector.onTouchEvent(ev) + + private fun onActionDown(ev: MotionEvent): Boolean { + if (!canInterceptTouch(ev)) { + return false + } + taskBeingDragged = + recentsView.taskViews.firstOrNull { + recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev) + } + // If event is not over a taskView, check if it would have been either over the + // currently dismissing task being dragged, or over where the next task will be. + ?: recentsView.taskViews.firstOrNull { taskView -> + if (!recentsView.isTaskViewVisible(taskView)) return@firstOrNull false + container.dragLayer.getDescendantRectRelativeToSelf( + taskView, + tempTaskThumbnailBounds, + ) + if (taskView == taskBeingDragged && !isDismissing) { + val secondaryTranslation = + -taskView.secondaryDismissTranslationProperty.get(taskView).toInt() + recentsView.pagedOrientationHandler.extendRectForSecondaryTranslation( + tempTaskThumbnailBounds, + secondaryTranslation, + ) + } else { + val primaryTranslation = + recentsView.taskViewsDismissPrimaryTranslations[taskView] ?: 0 + recentsView.pagedOrientationHandler.extendRectForPrimaryTranslation( + tempTaskThumbnailBounds, + primaryTranslation, + ) + } + tempTaskThumbnailBounds.contains(ev.x.toInt(), ev.y.toInt()) + } + + if (taskBeingDragged == null) { + debugLog(TAG, "Not intercepting touch, null dragged task.") + return false + } + val secondaryLayerDimension = + recentsView.pagedOrientationHandler.getSecondaryDimension(container.dragLayer) + // Dismiss length as bottom of task so it is fully off screen when dismissed. + // Take into account the recents scale when fully zoomed out on dismiss. + taskBeingDragged?.getThumbnailBounds(tempTaskThumbnailBounds, relativeToDragLayer = true) + dismissLength = + ceil( + recentsView.pagedOrientationHandler.getTaskDismissLength( + secondaryLayerDimension, + tempTaskThumbnailBounds, + ) / RECENTS_SCALE_ON_DISMISS_SUCCESS + ) + .toInt() + verticalFactor = recentsView.pagedOrientationHandler.getTaskDismissVerticalDirection() + taskBeingDragged?.isBeingDraggedForDismissal = true + + detector.setDetectableScrollConditions(upDirection, /* ignoreSlop= */ false) + return true + } + + override fun onDragStart(start: Boolean, startDisplacement: Float) { + val taskBeingDragged = taskBeingDragged ?: return + debugLog(TAG, "Handling touch event.") + + initialDisplacement = + taskBeingDragged.secondaryDismissTranslationProperty.get(taskBeingDragged) + taskDragDisplacementValue = + generateMotionValue( + initialDisplacement, + detachThreshold * verticalFactor, + container.asContext(), + ) { currentDisplacement -> + taskBeingDragged.secondaryDismissTranslationProperty.setValue( + taskBeingDragged, + currentDisplacement, + ) + if (taskBeingDragged.isRunningTask && recentsView.enableDrawingLiveTile) { + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value = + currentDisplacement + } + recentsView.redrawLiveTile() + } + } + + // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant + // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we + // want the dragged task to stay above all other views. + taskBeingDragged.translationZ = 0.1f + } + + override fun onDrag(displacement: Float): Boolean { + taskBeingDragged ?: return false + val currentDisplacement = displacement + initialDisplacement + val boundedDisplacement = + boundToRange(abs(currentDisplacement), 0f, dismissLength.toFloat()) + // When swiping below origin, allow slight undershoot to simulate resisting the movement. + val isAboveOrigin = + recentsView.pagedOrientationHandler.isGoingUp(currentDisplacement, isRtl) + val totalDisplacement = + if (isAboveOrigin) boundedDisplacement * verticalFactor + else + mapToRange( + boundedDisplacement, + 0f, + dismissLength.toFloat(), + 0f, + maxUndershoot, + DECELERATE, + ) * -verticalFactor + val dismissFraction = displacement / (dismissLength * verticalFactor).toFloat() + taskDragDisplacementValue?.input = totalDisplacement + RECENTS_SCALE_PROPERTY.setValue(recentsView, getRecentsScale(dismissFraction)) + playDismissThresholdHaptic(displacement) + return true + } + + /** + * Play a haptic to alert the user they have passed the dismiss threshold. + * + *

Check within a range of the threshold value, as the drag event does not necessarily happen + * at the exact threshold's displacement. + */ + private fun playDismissThresholdHaptic(displacement: Float) { + val dismissThreshold = (DISMISS_THRESHOLD_FRACTION * dismissLength * verticalFactor) + val inHapticRange = + displacement >= (dismissThreshold - DISMISS_THRESHOLD_HAPTIC_RANGE) && + displacement <= (dismissThreshold + DISMISS_THRESHOLD_HAPTIC_RANGE) + if (!inHapticRange) { + hasDismissThresholdHapticRun = false + } else if (!hasDismissThresholdHapticRun) { + MSDLPlayerWrapper.INSTANCE.get(recentsView.context) + .playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR) + hasDismissThresholdHapticRun = true + } + } + + override fun onDragEnd(velocity: Float) { + val taskBeingDragged = taskBeingDragged ?: return + taskDragDisplacementValue?.dispose() + taskBeingDragged.isBeingDraggedForDismissal = false + + val currentDisplacement = + taskBeingDragged.secondaryDismissTranslationProperty.get(taskBeingDragged) + val isBeyondDismissThreshold = + abs(currentDisplacement) > abs(DISMISS_THRESHOLD_FRACTION * dismissLength) + val velocityIsGoingUp = recentsView.pagedOrientationHandler.isGoingUp(velocity, isRtl) + val isFlingingTowardsDismiss = detector.isFling(velocity) && velocityIsGoingUp + val isFlingingTowardsRestState = detector.isFling(velocity) && !velocityIsGoingUp + isDismissing = + isFlingingTowardsDismiss || (isBeyondDismissThreshold && !isFlingingTowardsRestState) + val dismissThreshold = (DISMISS_THRESHOLD_FRACTION * dismissLength * verticalFactor).toInt() + val finalPosition = if (isDismissing) (dismissLength * verticalFactor).toFloat() else 0f + springAnimation = + recentsView.runTaskDismissSettlingSpringAnimation( + taskBeingDragged, + isDismissing, + RecentsDismissUtils.DismissedTaskData( + startVelocity = velocity, + dismissLength = dismissLength, + finalPosition = finalPosition, + dismissThreshold = dismissThreshold, + ), + /* shouldRemoveTaskView= */ isDismissing, + /* isSplitSelection= */ false, + ) + recentsScaleAnimation = + recentsView.animateRecentsScale(RECENTS_SCALE_DEFAULT).addEndListener { _, _, _, _ -> + recentsScaleAnimation = null + } + } + + private fun clearState() { + detector.finishedScrolling() + detector.setDetectableScrollConditions(0, false) + taskBeingDragged?.resetViewTransforms() + taskBeingDragged = null + springAnimation = null + taskDragDisplacementValue = null + isDismissing = false + } + + private fun getRecentsScale(dismissFraction: Float): Float { + return when { + // Do not scale recents when dragging below origin. + dismissFraction <= 0 -> { + RECENTS_SCALE_DEFAULT + } + // Initially scale recents as the drag begins, up to the first threshold. + dismissFraction < RECENTS_SCALE_FIRST_THRESHOLD_FRACTION -> { + mapToRange( + dismissFraction, + 0f, + RECENTS_SCALE_FIRST_THRESHOLD_FRACTION, + RECENTS_SCALE_DEFAULT, + RECENTS_SCALE_ON_DISMISS_CANCEL, + LINEAR, + ) + } + // Keep scale consistent until dragging to the dismiss threshold. + dismissFraction < RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION -> { + RECENTS_SCALE_ON_DISMISS_CANCEL + } + // Scale beyond the dismiss threshold again, to indicate dismiss will occur on release. + dismissFraction < RECENTS_SCALE_SECOND_THRESHOLD_FRACTION -> { + mapToRange( + dismissFraction, + RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION, + RECENTS_SCALE_SECOND_THRESHOLD_FRACTION, + RECENTS_SCALE_ON_DISMISS_CANCEL, + RECENTS_SCALE_ON_DISMISS_SUCCESS, + LINEAR, + ) + } + // Keep scale beyond the dismiss threshold scaling consistent. + else -> { + RECENTS_SCALE_ON_DISMISS_SUCCESS + } + } + } + + private fun generateMotionValue( + initialDisplacement: Float, + detachThreshold: Float, + context: Context, + updateCallback: (Float) -> Unit, + ): ViewMotionValue { + val direction = if (initialDisplacement < 0) InputDirection.Max else InputDirection.Min + val distanceGestureContext = + DistanceGestureContext.create(context, initialDisplacement, direction) + val viewMotionValue = + ViewMotionValue( + initialDisplacement, + distanceGestureContext, + generateMotionSpec(detachThreshold), + label = "taskDismiss::displacement", + ) + + viewMotionValue.addUpdateCallback { motionValue -> updateCallback(motionValue.output) } + return viewMotionValue + } + + /** Motion spec for an initial magnetic detach. Track linearly otherwise. No reattach. */ + private fun generateMotionSpec(detachThreshold: Float): MotionSpec { + val spring = SpringParameters(stiffness = 800f, dampingRatio = 0.95f) + val detachKey = BreakpointKey("TaskDismiss::Detach") + val breakpoints = mutableListOf() + val mappings = mutableListOf() + + breakpoints.add(minLimit) + mappings.add(Mapping.Identity) + breakpoints.add(Breakpoint(detachKey, detachThreshold, spring, Guarantee.None)) + mappings.add(Mapping.Linear(MAGNETIC_DETACH_INTERPOLATION_FRACTION)) + breakpoints.add(maxLimit) + + return MotionSpec(DirectionalMotionSpec(breakpoints, mappings)) + } + + companion object { + private const val TAG = "TaskViewDismissTouchController" + + private const val DISMISS_THRESHOLD_FRACTION = 0.5f + private const val DISMISS_THRESHOLD_HAPTIC_RANGE = 10f + + private const val RECENTS_SCALE_ON_DISMISS_CANCEL = 0.9875f + private const val RECENTS_SCALE_ON_DISMISS_SUCCESS = 0.975f + private const val RECENTS_SCALE_DEFAULT = 1f + private const val RECENTS_SCALE_FIRST_THRESHOLD_FRACTION = 0.2f + private const val RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION = 0.5f + private const val RECENTS_SCALE_SECOND_THRESHOLD_FRACTION = 0.575f + + private const val MAGNETIC_DETACH_INTERPOLATION_FRACTION = 0.35f + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt new file mode 100644 index 0000000000..6225c47835 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides.touchcontrollers + +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import com.android.app.animation.Interpolators.ZOOM_IN +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.LauncherAnimUtils +import com.android.launcher3.Utilities.EDGE_NAV_BAR +import com.android.launcher3.Utilities.boundToRange +import com.android.launcher3.Utilities.debugLog +import com.android.launcher3.Utilities.isRtl +import com.android.launcher3.anim.AnimatorPlaybackController +import com.android.launcher3.touch.BaseSwipeDetector +import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.FlingBlockCheck +import com.android.launcher3.util.TouchController +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskView +import kotlin.math.abs + +/** Touch controller which handles dragging task view cards for launch. */ +class TaskViewLaunchTouchController( + private val container: CONTAINER, + private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext, +) : TouchController, SingleAxisSwipeDetector.Listener where +CONTAINER : Context, +CONTAINER : RecentsViewContainer { + private val tempRect = Rect() + private val flingBlockCheck = FlingBlockCheck() + private val recentsView: RecentsView<*, *> = container.getOverviewPanel() + private val detector: SingleAxisSwipeDetector = + SingleAxisSwipeDetector( + container as Context, + this, + recentsView.pagedOrientationHandler.upDownSwipeDirection, + ) + private val isRtl = isRtl(container.resources) + private val downDirection = recentsView.pagedOrientationHandler.getDownDirection(isRtl) + + private var taskBeingDragged: TaskView? = null + private var launchEndDisplacement: Float = 0f + private var playbackController: AnimatorPlaybackController? = null + private var verticalFactor: Int = 0 + private var canInterceptTouch = false + + private fun canTaskLaunchTaskView(taskView: TaskView?) = + taskView != null && + taskView === recentsView.currentPageTaskView && + DisplayController.getNavigationMode(container).hasGestures && + (!recentsView.showAsGrid() || taskView.isLargeTile) && + recentsView.isTaskInExpectedScrollPosition(taskView) + + private fun canInterceptTouch(ev: MotionEvent): Boolean = + when { + // Don't intercept swipes on the nav bar, as user might be trying to go home during a + // task dismiss animation. + (ev.edgeFlags and EDGE_NAV_BAR) != 0 -> { + debugLog(TAG, "Not intercepting edge swipe on nav bar.") + false + } + + // Floating views that a TouchController should not try to intercept touches from. + AbstractFloatingView.getTopOpenViewWithType( + container, + AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT, + ) != null -> { + debugLog(TAG, "Not intercepting, open floating view blocking touch.") + false + } + + // Disable swiping if the task overlay is modal. + taskViewRecentsTouchContext.isRecentsModal -> { + debugLog(TAG, "Not intercepting touch in modal overlay.") + false + } + + // Do not allow launch while recents is scrolling. + !recentsView.scroller.isFinished -> { + debugLog(TAG, "Not intercepting touch, recents scrolling.") + false + } + + else -> + taskViewRecentsTouchContext.isRecentsInteractive.also { isRecentsInteractive -> + if (!isRecentsInteractive) { + debugLog(TAG, "Not intercepting touch, recents not interactive.") + } + } + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + if ( + (ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL) && + playbackController == null + ) { + clearState() + } + if (ev.action == MotionEvent.ACTION_DOWN) { + canInterceptTouch = onActionDown(ev) + if (!canInterceptTouch) { + clearState() + return false + } + } + // Ignore other actions if touch intercepting has not been enabled in an ACTION_DOWN event. + if (!canInterceptTouch) { + return false + } + onControllerTouchEvent(ev) + val downDirectionIsNegative = downDirection == SingleAxisSwipeDetector.DIRECTION_NEGATIVE + val wasInitialTouchDown = + (downDirectionIsNegative && !detector.wasInitialTouchPositive()) || + (!downDirectionIsNegative && detector.wasInitialTouchPositive()) + return detector.isDraggingState && wasInitialTouchDown + } + + override fun onControllerTouchEvent(ev: MotionEvent) = detector.onTouchEvent(ev) + + private fun onActionDown(ev: MotionEvent): Boolean { + if (!canInterceptTouch(ev)) { + return false + } + taskBeingDragged = + recentsView.taskViews + .firstOrNull { + recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev) + } + ?.also { + verticalFactor = + recentsView.pagedOrientationHandler.getTaskDragDisplacementFactor(isRtl) + } + if (!canTaskLaunchTaskView(taskBeingDragged)) { + debugLog(TAG, "Not intercepting touch, task cannot be launched.") + return false + } + detector.setDetectableScrollConditions(downDirection, /* ignoreSlop= */ false) + return true + } + + override fun onDragStart(start: Boolean, startDisplacement: Float) { + val taskBeingDragged = taskBeingDragged ?: return + debugLog(TAG, "Handling touch event.") + + val secondaryLayerDimension: Int = + recentsView.pagedOrientationHandler.getSecondaryDimension(container.getDragLayer()) + val maxDuration = 2L * secondaryLayerDimension + recentsView.clearPendingAnimation() + val pendingAnimation = + recentsView.createTaskLaunchAnimation(taskBeingDragged, maxDuration, ZOOM_IN) + // Since the thumbnail is what is filling the screen, based the end displacement on it. + taskBeingDragged.getThumbnailBounds(tempRect, /* relativeToDragLayer= */ true) + launchEndDisplacement = + recentsView.pagedOrientationHandler + .getTaskLaunchLength(secondaryLayerDimension, tempRect) + .toFloat() * verticalFactor + playbackController = + pendingAnimation.createPlaybackController()?.apply { + taskViewRecentsTouchContext.onUserControlledAnimationCreated(this) + dispatchOnStart() + } + } + + override fun onDrag(displacement: Float): Boolean { + playbackController?.setPlayFraction( + boundToRange(displacement / launchEndDisplacement, 0f, 1f) + ) + return true + } + + override fun onDragEnd(velocity: Float) { + val playbackController = playbackController ?: return + + val isBeyondLaunchThreshold = + abs(playbackController.progressFraction) > abs(LAUNCH_THRESHOLD_FRACTION) + val velocityIsNegative = !recentsView.pagedOrientationHandler.isGoingUp(velocity, isRtl) + val isFlingingTowardsLaunch = detector.isFling(velocity) && velocityIsNegative + val isFlingingTowardsRestState = detector.isFling(velocity) && !velocityIsNegative + val isLaunching = + isFlingingTowardsLaunch || (isBeyondLaunchThreshold && !isFlingingTowardsRestState) + + val progress = playbackController.progressFraction + var animationDuration = + BaseSwipeDetector.calculateDuration( + velocity, + if (isLaunching) (1 - progress) else progress, + ) + if (detector.isFling(velocity) && flingBlockCheck.isBlocked && !isLaunching) { + animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity).toLong() + } + + playbackController.setEndAction(this::clearState) + playbackController.startWithVelocity( + container, + isLaunching, + velocity, + launchEndDisplacement, + animationDuration, + ) + } + + private fun clearState() { + detector.finishedScrolling() + detector.setDetectableScrollConditions(0, false) + taskBeingDragged = null + playbackController = null + } + + companion object { + private const val TAG = "TaskViewLaunchTouchController" + private const val LAUNCH_THRESHOLD_FRACTION: Float = 0.5f + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewRecentsTouchContext.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewRecentsTouchContext.java new file mode 100644 index 0000000000..e8d31c183a --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewRecentsTouchContext.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides.touchcontrollers; + +import com.android.launcher3.anim.AnimatorPlaybackController; + +/** Interface providing context about the RecentsView state to a {@link TaskViewTouchController}. */ +public interface TaskViewRecentsTouchContext { + /** Returns whether Recents is interactive for touch. */ + boolean isRecentsInteractive(); + + /** Returns if Recents is showing a single task in a modal way. */ + boolean isRecentsModal(); + + /** Runs when a user controlled animation is created. */ + default void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java similarity index 87% rename from quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java rename to quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java index 205da70513..d9b0366a40 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java @@ -17,14 +17,15 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT; import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; +import static com.android.launcher3.Utilities.debugLog; import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; +import android.graphics.Rect; import android.os.VibrationEffect; import android.view.MotionEvent; -import android.view.View; import android.view.animation.Interpolator; import com.android.app.animation.Interpolators; @@ -49,10 +50,14 @@ import com.android.quickstep.views.TaskView; /** * Touch controller for handling task view card swipes + * + * @deprecated This class will be replaced by the new {@link TaskViewTouchController}. */ -public abstract class TaskViewTouchController - extends AnimatorListenerAdapter implements TouchController, - SingleAxisSwipeDetector.Listener { +@Deprecated +public class TaskViewTouchControllerDeprecated< + CONTAINER extends Context & RecentsViewContainer> extends AnimatorListenerAdapter + implements TouchController, SingleAxisSwipeDetector.Listener { + private static final String TAG = "TaskViewTouchControllerDeprecated"; private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f; private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300; @@ -65,9 +70,10 @@ public abstract class TaskViewTouchController mRecentsView; + private final Rect mTempRect = new Rect(); private final boolean mIsRtl; private AnimatorPlaybackController mCurrentAnimation; @@ -88,8 +94,10 @@ public abstract class TaskViewTouchController 0) { // inclusion filter + (widgetCategory and categoryMask) != 0 + } else if (categoryMask < 0) { // exclusion filter + (widgetCategory and categoryMask) == widgetCategory + } else { + true // no filter + } + } +} diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index fe3c35251b..aa81afb759 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -16,6 +16,7 @@ package com.android.quickstep; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -30,22 +31,26 @@ import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; import static com.android.launcher3.BaseActivity.EVENT_STARTED; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; -import static com.android.launcher3.Flags.enableAdditionalHomeAnimations; -import static com.android.launcher3.Flags.enableGridOnlyOverview; +import static com.android.launcher3.Flags.enableGestureNavHorizontalTouchSlop; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; +import static com.android.launcher3.Flags.msdlFeedback; import static com.android.launcher3.PagedView.INVALID_PAGE; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_ENTER_DESKTOP_MODE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_EXIT_DESKTOP_MODE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT; +import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; +import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; -import static com.android.quickstep.GestureState.GestureEndTarget.ALL_APPS; +import static com.android.quickstep.BaseContainerInterface.AnimationFactory; import static com.android.quickstep.GestureState.GestureEndTarget.HOME; import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK; import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK; @@ -53,15 +58,14 @@ import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS; import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED; import static com.android.quickstep.GestureState.STATE_END_TARGET_SET; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELED; +import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED; import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; -import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION; +import static com.android.quickstep.TaskViewUtils.extractTargetsAndStates; import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED; -import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP; -import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED; -import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -80,6 +84,9 @@ import android.graphics.RectF; import android.os.IBinder; import android.os.SystemClock; import android.util.Log; +import android.util.Pair; +import android.util.TimeUtils; +import android.view.Choreographer; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; @@ -91,39 +98,51 @@ import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.WindowInsets; import android.view.animation.Interpolator; import android.widget.Toast; +import android.window.DesktopExperienceFlags; +import android.window.DesktopModeFlags; import android.window.PictureInPictureSurfaceTransaction; +import android.window.TransitionInfo; +import android.window.WindowAnimationState; +import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; import com.android.internal.jank.Cuj; import com.android.internal.util.LatencyTracker; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.BaseState; +import com.android.launcher3.statemanager.StatefulContainer; import com.android.launcher3.taskbar.TaskbarThresholdUtils; import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.MSDLPlayerWrapper; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.util.WindowBounds; -import com.android.quickstep.BaseActivityInterface.AnimationFactory; import com.android.quickstep.GestureState.GestureEndTarget; import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.util.ActiveGestureErrorDetector; import com.android.quickstep.util.ActiveGestureLog; -import com.android.quickstep.util.ActivityInitListener; +import com.android.quickstep.util.ActiveGestureProtoLogProxy; import com.android.quickstep.util.AnimatorControllerWithResistance; +import com.android.quickstep.util.ContextInitListener; import com.android.quickstep.util.InputConsumerProxy; import com.android.quickstep.util.InputProxyHandlerFactory; import com.android.quickstep.util.MotionPauseDetector; @@ -138,9 +157,11 @@ import com.android.quickstep.util.TransformParams; import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; +import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; -import com.android.quickstep.views.TaskView.TaskContainer; -import com.android.systemui.shared.recents.model.Task; +import com.android.quickstep.views.TaskViewType; +import com.android.systemui.animation.TransitionAnimator; +import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputConsumerController; @@ -148,9 +169,13 @@ import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; -import com.android.window.flags2.Flags; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils; +import com.android.wm.shell.shared.GroupedTaskInfo; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.pip.PipFlags; +import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils; + +import com.google.android.msdl.data.model.MSDLToken; import kotlin.Unit; @@ -158,7 +183,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.function.Consumer; @@ -166,8 +190,9 @@ import java.util.function.Consumer; /** * Handles the navigation gestures when Launcher is the default home activity. */ -public abstract class AbsSwipeUpHandler> +public abstract class AbsSwipeUpHandler< + RECENTS_CONTAINER extends Context & RecentsViewContainer & StatefulContainer, + RECENTS_VIEW extends RecentsView, STATE extends BaseState> extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener, RecentsAnimationCallbacks.RecentsAnimationListener { private static final String TAG = "AbsSwipeUpHandler"; @@ -177,30 +202,26 @@ public abstract class AbsSwipeUpHandler mContainerInterface; + protected final RecentsAnimationDeviceState mDeviceState; + protected final BaseContainerInterface mContainerInterface; protected final InputConsumerProxy mInputConsumerProxy; - protected final ActivityInitListener mActivityInitListener; + protected final ContextInitListener mContextInitListener; // Callbacks to be made once the recents animation starts private final ArrayList mRecentsAnimationStartCallbacks = new ArrayList<>(); private final OnScrollChangedListener mOnRecentsScrollListener = this::onRecentsViewScroll; // Null if the recents animation hasn't started yet or has been canceled or finished. protected @Nullable RecentsAnimationController mRecentsAnimationController; - protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController; protected RecentsAnimationTargets mRecentsAnimationTargets; - protected @Nullable T mContainer; - protected @Nullable Q mRecentsView; + protected @Nullable RECENTS_CONTAINER mContainer; + protected @Nullable RECENTS_VIEW mRecentsView; protected Runnable mGestureEndCallback; + protected Runnable mGestureAnimationEndCallback; protected MultiStateCallback mStateCallback; protected boolean mCanceled; private boolean mRecentsViewScrollLinked = false; - - private final Runnable mLauncherOnDestroyCallback = () -> { - ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED); - mRecentsView = null; - mContainer = null; - mStateCallback.clearState(STATE_LAUNCHER_PRESENT); - }; + // The previous task view type before the user quick switches between tasks + private TaskViewType mPreviousTaskViewType; private static int FLAG_COUNT = 0; private static int getNextStateFlag(String name) { @@ -260,8 +281,6 @@ public abstract class AbsSwipeUpHandler { + ActiveGestureProtoLogProxy.logLauncherDestroyed(); + mContextInitListener.unregister("AbsSwipeUpHandler.mLauncherOnDestroyCallback"); + if (mRecentsView != null) { + mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener); + mRecentsView = null; + } + mContainer = null; + mStateCallback.clearState(STATE_LAUNCHER_PRESENT); + mRecentsAnimationStartCallbacks.clear(); + mTaskAnimationManager.onLauncherDestroyed(); + }; mInputConsumerProxy = new InputConsumerProxy(context, /* rotationSupplier = */ () -> { if (mRecentsView == null) { @@ -369,8 +409,10 @@ public abstract class AbsSwipeUpHandler snapshots = mGestureState.consumeRecentsAnimationCanceledSnapshot(); if (snapshots != null) { - mRecentsView.switchToScreenshot(snapshots, () -> { - if (mRecentsAnimationController != null) { - mRecentsAnimationController.cleanupScreenshot(); - } else if (mDeferredCleanupRecentsAnimationController != null) { - mDeferredCleanupRecentsAnimationController.cleanupScreenshot(); - mDeferredCleanupRecentsAnimationController = null; - } - }); + mRecentsView.switchToScreenshot(snapshots, () -> {}); mRecentsView.onRecentsAnimationComplete(); } }); @@ -560,8 +597,8 @@ public abstract class AbsSwipeUpHandler { - mAnimationFactory = mContainerInterface.prepareRecentsUI(mDeviceState, + mAnimationFactory = mContainerInterface.prepareRecentsUI( mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated); maybeUpdateRecentsAttachedState(false /* animate */); if (mGestureState.getEndTarget() != null) { @@ -644,11 +681,8 @@ public abstract class AbsSwipeUpHandler { - mDeviceState.getRotationTouchHelper() - .onEndTargetCalculated(mGestureState.getEndTarget(), - mContainerInterface); - }); + () -> mRotationTouchHelper.onEndTargetCalculated(mGestureState.getEndTarget(), + mContainerInterface)); notifyGestureStarted(); } @@ -669,26 +703,27 @@ public abstract class AbsSwipeUpHandler - remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(isInAllAppsRegion)); - } - - private void buildAnimationController() { if (!canCreateNewOrUpdateExistingLauncherTransitionController()) { return; } initTransitionEndpoints(mContainer.getDeviceProfile()); - mAnimationFactory.createActivityInterface(mTransitionDragLength); + mAnimationFactory.createContainerInterface(mTransitionDragLength); } /** @@ -890,18 +906,22 @@ public abstract class AbsSwipeUpHandler= threshold); updateSysUiFlags(mCurrentShift.value); applyScrollAndTransform(); @@ -928,14 +948,13 @@ public abstract class AbsSwipeUpHandler 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; boolean quickswitchThresholdPassed = centermostTask != runningTask; // We will handle the sysui flags based on the centermost task view. mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed || (quickswitchThresholdPassed && centermostTaskFlags != 0)); - mRecentsAnimationController.setSplitScreenMinimized(mContext, swipeUpThresholdPassed); // Provide a hint to WM the direction that we will be settling in case the animation // needs to be canceled mRecentsAnimationController.setWillFinishToHome(swipeUpThresholdPassed); @@ -952,10 +971,24 @@ public abstract class AbsSwipeUpHandler thumbnailDatas) { - ActiveGestureLog.INSTANCE.addLog( - /* event= */ "cancelRecentsAnimation", - /* gestureEvent= */ CANCEL_RECENTS_ANIMATION); - mActivityInitListener.unregister("AbsSwipeUpHandler.onRecentsAnimationCanceled"); - // Cache the recents animation controller so we can defer its cleanup to after having - // properly cleaned up the screenshot without accidentally using it. - mDeferredCleanupRecentsAnimationController = mRecentsAnimationController; + ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerOnRecentsAnimationCanceled(); + mContextInitListener.unregister("AbsSwipeUpHandler.onRecentsAnimationCanceled"); mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); // Defer clearing the controller and the targets until after we've updated the state mRecentsAnimationController = null; @@ -1039,12 +1073,10 @@ public abstract class AbsSwipeUpHandler rv.getViewTreeObserver().removeOnDrawListener(this)); } @@ -1084,7 +1116,7 @@ public abstract class AbsSwipeUpHandler handleNormalGestureEnd( - endVelocityPxPerMs, isFling, velocityPxPerMs, /* isCancel= */ false); + endVelocityPxPerMs, + isFling, + velocityPxPerMs, + /* isCancel= */ false, + horizontalTouchSlopPassed); if (mRecentsView != null) { mRecentsView.runOnPageScrollsInitialized(handleNormalGestureEndCallback); } else { @@ -1152,7 +1194,25 @@ public abstract class AbsSwipeUpHandler { - if (!updateThumbnail(false /* refreshView */)) { - setScreenshotCapturedState(); - } + updateThumbnail(); + setScreenshotCapturedState(); }); }); return; } - finishTransitionPosted = updateThumbnail(false /* refreshView */); + updateThumbnail(); } - if (!finishTransitionPosted) { - setScreenshotCapturedState(); - } + setScreenshotCapturedState(); } } // Returns whether finish transition was posted. - private boolean updateThumbnail(boolean refreshView) { + private void updateThumbnail() { if (mGestureState.getEndTarget() == HOME || mGestureState.getEndTarget() == NEW_TASK - || mGestureState.getEndTarget() == ALL_APPS || mRecentsView == null) { // Capture the screenshot before finishing the transition to home or quickswitching to // ensure it's taken in the correct orientation, but no need to update the thumbnail. - return false; + return; } - boolean finishTransitionPosted = false; - TaskView updatedTaskView = mRecentsView.updateThumbnail(mTaskSnapshotCache, refreshView); - if (updatedTaskView != null && refreshView && !mCanceled) { - // Defer finishing the animation until the next launcher frame with the - // new thumbnail - finishTransitionPosted = ViewUtils.postFrameDrawn(updatedTaskView, - () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED), - this::isCanceled); - } - - return finishTransitionPosted; + mRecentsView.updateThumbnail(mTaskSnapshotCache); } private void setScreenshotCapturedState() { @@ -2221,8 +2343,9 @@ public abstract class AbsSwipeUpHandler getRecentsViewDispatcher(float navbarRotation) { return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null; } @@ -2272,13 +2403,17 @@ public abstract class AbsSwipeUpHandler remoteTargetHandle.getTransformParams() - .setSyncTransactionApplier(applier)); + .setSyncTransactionApplier(applier)); runOnRecentsAnimationAndLauncherBound(() -> mRecentsAnimationTargets.addReleaseCheck(applier)); @@ -2291,9 +2426,9 @@ public abstract class AbsSwipeUpHandler mGestureState.getPreviouslyAppearedTaskIds() - .contains(taskId)); + taskId -> mGestureState.getPreviouslyAppearedTaskIds() + .contains(taskId)); if (!hasTaskPreviouslyAppeared) { ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED); } - ActiveGestureLog.INSTANCE.addLog(nextTaskLog); - nextTask.launchTask(success -> { + ActiveGestureProtoLogProxy.logStartNewTask(nextTaskLog); + nextTask.launchWithoutAnimation(true, success -> { resultCallback.accept(success); if (success) { if (hasTaskPreviouslyAppeared) { @@ -2361,7 +2490,7 @@ public abstract class AbsSwipeUpHandler { + ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerOnTasksAppeared(); + mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); + }; + ActiveGestureLog.CompoundString forceFinishReason = + ActiveGestureLog.CompoundString.newEmptyString(); + if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED) + && !hasStartedTaskBefore(appearedTaskTargets)) { // This is a special case, if a task is started mid-gesture that wasn't a part of a // previous quickswitch task launch, then cancel the animation back to the app RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0]; TaskInfo taskInfo = appearedTaskTarget.taskInfo; - ActiveGestureLog.INSTANCE.addLog( - new ActiveGestureLog.CompoundString("Unexpected task appeared") - .append(" id=") - .append(taskInfo.taskId) - .append(" pkg=") - .append(taskInfo.baseIntent.getComponent().getPackageName())); - finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); + ActiveGestureProtoLogProxy.logUnexpectedTaskAppeared( + taskInfo.taskId, + taskInfo.baseIntent.getComponent().getPackageName()); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } - if (!handleTaskAppeared(appearedTaskTargets)) { + ActiveGestureLog.CompoundString handleTaskFailureReason = + ActiveGestureLog.CompoundString.newEmptyString(); + if (!handleTaskAppeared(appearedTaskTargets, handleTaskFailureReason)) { + forceFinishReason.append(handleTaskFailureReason); + ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } - Optional taskTargetOptional = - Arrays.stream(appearedTaskTargets) - .filter(mGestureState.mLastStartedTaskIdPredicate) - .findFirst(); - if (!taskTargetOptional.isPresent()) { - ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id"); - finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); + RemoteAnimationTarget[] taskTargets = Arrays.stream(appearedTaskTargets) + .filter(mGestureState.mLastStartedTaskIdPredicate) + .toArray(RemoteAnimationTarget[]::new); + if (taskTargets.length == 0) { + forceFinishReason.append("No appeared task matching started task id"); + ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } - RemoteAnimationTarget taskTarget = taskTargetOptional.get(); + RemoteAnimationTarget taskTarget = taskTargets[0]; TaskView taskView = mRecentsView == null ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId); - if (taskView == null - || !taskView.getFirstThumbnailViewDeprecated().shouldShowSplashView()) { - ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state"); - finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); + if (taskView == null || taskView.getTaskContainers().stream().noneMatch( + TaskContainer::getShouldShowSplashView)) { + forceFinishReason.append("Splash not needed"); + ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } if (mContainer == null) { - ActiveGestureLog.INSTANCE.addLog("Activity destroyed"); - finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); + forceFinishReason.append("Activity destroyed"); + ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason); + finishRecentsAnimationOnTasksAppeared(onFinishComplete); return; } - animateSplashScreenExit(mContainer, appearedTaskTargets, taskTarget.leash); + animateSplashScreenExit(mContainer, appearedTaskTargets, taskTargets); } private void animateSplashScreenExit( - @NonNull T activity, + @NonNull RECENTS_CONTAINER activity, @NonNull RemoteAnimationTarget[] appearedTaskTargets, - @NonNull SurfaceControl leash) { + @NonNull RemoteAnimationTarget[] animatingTargets) { ViewGroup splashView = activity.getDragLayer(); final QuickstepLauncher quickstepLauncher = activity instanceof QuickstepLauncher ? (QuickstepLauncher) activity : null; @@ -2485,33 +2629,35 @@ public abstract class AbsSwipeUpHandler splashView.setAlpha(1)); } - finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1)); - } - }); + }); + } } private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) { if (mRecentsAnimationController != null) { mRecentsAnimationController.finish(false /* toRecents */, onFinishComplete); } - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared"); + ActiveGestureProtoLogProxy.logFinishRecentsAnimationOnTasksAppeared(); } /** @@ -2546,7 +2692,7 @@ public abstract class AbsSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs); } } diff --git a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt index 6fd68d551e..51a68d16da 100644 --- a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt +++ b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt @@ -21,10 +21,17 @@ import android.app.PendingIntent import android.app.RemoteAction import android.content.Context import android.graphics.drawable.Icon +import android.provider.Settings +import android.provider.Settings.Secure.USER_SETUP_COMPLETE import android.view.accessibility.AccessibilityManager import com.android.launcher3.R +import com.android.launcher3.util.SettingsCache +import com.android.launcher3.util.SettingsCache.OnChangeListener +import com.android.quickstep.input.QuickstepKeyGestureEventsManager import java.util.concurrent.Executor +private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPLETE) + /** * Registers a [RemoteAction] for toggling All Apps if needed. * @@ -35,9 +42,16 @@ import java.util.concurrent.Executor class AllAppsActionManager( private val context: Context, private val bgExecutor: Executor, + private val quickstepKeyGestureEventsManager: QuickstepKeyGestureEventsManager, private val createAllAppsPendingIntent: () -> PendingIntent, ) { + private val onSettingsChangeListener = OnChangeListener { v -> isUserSetupComplete = v } + + init { + SettingsCache.INSTANCE[context].register(USER_SETUP_COMPLETE_URI, onSettingsChangeListener) + } + /** `true` if home and overview are the same Activity. */ var isHomeAndOverviewSame = false set(value) { @@ -52,12 +66,27 @@ class AllAppsActionManager( updateSystemAction() } + /** `true` if the setup UI is visible. */ + var isSetupUiVisible = false + set(value) { + field = value + updateSystemAction() + } + + private var isUserSetupComplete = + SettingsCache.INSTANCE[context].getValue(USER_SETUP_COMPLETE_URI, 0) + set(value) { + field = value + updateSystemAction() + } + /** `true` if the action should be registered. */ var isActionRegistered = false private set private fun updateSystemAction() { - val shouldRegisterAction = isHomeAndOverviewSame || isTaskbarPresent + val isInSetupFlow = isSetupUiVisible || !isUserSetupComplete + val shouldRegisterAction = (isHomeAndOverviewSame || isTaskbarPresent) && !isInSetupFlow if (isActionRegistered == shouldRegisterAction) return isActionRegistered = shouldRegisterAction @@ -65,17 +94,22 @@ class AllAppsActionManager( val accessibilityManager = context.getSystemService(AccessibilityManager::class.java) ?: return@execute if (shouldRegisterAction) { + val allAppsPendingIntent = createAllAppsPendingIntent() accessibilityManager.registerSystemAction( RemoteAction( Icon.createWithResource(context, R.drawable.ic_apps), context.getString(R.string.all_apps_label), context.getString(R.string.all_apps_label), - createAllAppsPendingIntent(), + allAppsPendingIntent, ), GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, ) + quickstepKeyGestureEventsManager.registerAllAppsKeyGestureEvent( + allAppsPendingIntent + ) } else { accessibilityManager.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS) + quickstepKeyGestureEventsManager.unregisterAllAppsKeyGestureEvent() } } } @@ -84,8 +118,11 @@ class AllAppsActionManager( isActionRegistered = false context .getSystemService(AccessibilityManager::class.java) - ?.unregisterSystemAction( - GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, - ) + ?.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS) + quickstepKeyGestureEventsManager.unregisterAllAppsKeyGestureEvent() + SettingsCache.INSTANCE[context].unregister( + USER_SETUP_COMPLETE_URI, + onSettingsChangeListener, + ) } } diff --git a/quickstep/src/com/android/quickstep/AspectRatioSystemShortcut.kt b/quickstep/src/com/android/quickstep/AspectRatioSystemShortcut.kt new file mode 100644 index 0000000000..f2242d8d4b --- /dev/null +++ b/quickstep/src/com/android/quickstep/AspectRatioSystemShortcut.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.content.Intent +import android.provider.Settings +import android.view.View +import androidx.core.net.toUri +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.popup.SystemShortcut +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.window.flags.Flags.universalResizableByDefault + +/** + * System shortcut to change the application's aspect ratio compatibility mode. + * + * This shows up only on screens that are not compact, ie. shortest-width greater than {@link + * com.android.launcher3.util.window.WindowManagerProxy#MIN_TABLET_WIDTH}. + */ +class AspectRatioSystemShortcut( + viewContainer: RecentsViewContainer, + taskContainer: TaskContainer, + abstractFloatingViewHelper: AbstractFloatingViewHelper, +) : + SystemShortcut( + R.drawable.ic_aspect_ratio, + R.string.recent_task_option_aspect_ratio, + viewContainer, + taskContainer.itemInfo, + taskContainer.taskView, + abstractFloatingViewHelper, + ) { + override fun onClick(view: View) { + dismissTaskMenuView() + + val intent = + Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + if (mItemInfo.targetPackage != null) { + intent.setData(("package:" + mItemInfo.targetPackage).toUri()) + } + + mTarget.startActivitySafely(view, intent, mItemInfo) + mTarget.statsLogManager + .logger() + .withItemInfo(mItemInfo) + .log(LauncherEvent.LAUNCHER_ASPECT_RATIO_SETTINGS_SYSTEM_SHORTCUT_TAP) + } + + companion object { + /** Optionally create a factory for the aspect ratio system shortcut. */ + @JvmOverloads + fun createFactory( + abstractFloatingViewHelper: AbstractFloatingViewHelper = AbstractFloatingViewHelper() + ): TaskShortcutFactory { + return object : TaskShortcutFactory { + override fun getShortcuts( + viewContainer: RecentsViewContainer, + taskContainer: TaskContainer, + ): List? { + return when { + // Only available when the feature flag is on. + !universalResizableByDefault() -> null + + // The option is only shown on sw600dp+ screens (checked by isTablet) + !viewContainer.deviceProfile.deviceProperties.isTablet -> null + + else -> { + listOf( + AspectRatioSystemShortcut( + viewContainer, + taskContainer, + abstractFloatingViewHelper, + ) + ) + } + } + } + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java index 00cd60b1d6..c387856245 100644 --- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java +++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java @@ -18,29 +18,25 @@ package com.android.quickstep; import static com.android.app.animation.Interpolators.ACCELERATE_2; import static com.android.app.animation.Interpolators.INSTANT; import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe; import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION; -import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK; +import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_ATTACHED_ALPHA_ANIM; import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM; import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; +import static com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.graphics.Color; -import android.view.MotionEvent; import androidx.annotation.Nullable; -import androidx.annotation.UiThread; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.statehandlers.DepthController; -import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.taskbar.TaskbarUIController; @@ -61,48 +57,14 @@ import java.util.function.Consumer; public abstract class BaseActivityInterface, ACTIVITY_TYPE extends StatefulActivity & RecentsViewContainer> extends BaseContainerInterface { - private final STATE_TYPE mBackgroundState; private STATE_TYPE mTargetState; - @Nullable private Runnable mOnInitBackgroundStateUICallback = null; - protected BaseActivityInterface(boolean rotationSupportedByActivity, STATE_TYPE overviewState, STATE_TYPE backgroundState) { + super(backgroundState); this.rotationSupportedByActivity = rotationSupportedByActivity; mTargetState = overviewState; - mBackgroundState = backgroundState; - } - - /** - * Called when the current gesture transition is cancelled. - * @param activityVisible Whether the user can see the changes we make here, so try to animate. - * @param endTarget If the gesture ended before we got cancelled, where we were headed. - */ - public void onTransitionCancelled(boolean activityVisible, - @Nullable GestureState.GestureEndTarget endTarget) { - ACTIVITY_TYPE activity = getCreatedContainer(); - if (activity == null) { - return; - } - STATE_TYPE startState = activity.getStateManager().getRestState(); - if (endTarget != null) { - // We were on our way to this state when we got canceled, end there instead. - startState = stateFromGestureEndTarget(endTarget); - DesktopVisibilityController controller = getDesktopVisibilityController(); - if (controller != null && controller.areDesktopTasksVisible() - && endTarget == LAST_TASK) { - // When we are cancelling the transition and going back to last task, move to - // rest state instead when desktop tasks are visible. - // If a fullscreen task is visible, launcher goes to normal state when the - // activity is stopped. This does not happen when desktop tasks are visible - // on top of launcher. Force the launcher state to rest state here. - startState = activity.getStateManager().getRestState(); - // Do not animate the transition - activityVisible = false; - } - } - activity.getStateManager().goToState(startState, activityVisible); } @Nullable @@ -123,21 +85,6 @@ public abstract class BaseActivityInterface T getVisibleRecentsView(); - - @UiThread - public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener); - - public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) { - TaskbarUIController controller = getTaskbarController(); - boolean isEventOverBubbleBarStashHandle = - controller != null && controller.isEventOverBubbleBarStashHandle(ev); - return deviceState.isInDeferredGestureRegion(ev) || deviceState.isImeRenderingNavButtons() - || isTrackpadMultiFingerSwipe(ev) || isEventOverBubbleBarStashHandle; - } - /** * Closes any overlays. */ @@ -162,47 +109,6 @@ public abstract class BaseActivityInterface

- * This method should only be used when the All Set page of the SUW is reached to safely - * preload the Launcher for the SUW first reveal. - */ - public void preloadOverviewForSUWAllSet() { - executeForTouchInteractionService(tis -> tis.preloadOverview(false, true)); + @BinderThread + @Override + public void onDisplayAddSystemDecorations(int displayId) { + executeForTouchInteractionService(tis -> + tis.mSystemDecorationChangeObserver.notifyAddSystemDecorations(displayId)); + } + + @BinderThread + @Override + public void onDisplayRemoved(int displayId) { + executeForTouchInteractionService(tis -> { + tis.mSystemDecorationChangeObserver.notifyOnDisplayRemoved(displayId); + }); + } + + @BinderThread + @Override + public void onDisplayRemoveSystemDecorations(int displayId) { + executeForTouchInteractionService(tis -> { + tis.mSystemDecorationChangeObserver.notifyDisplayRemoveSystemDecorations(displayId); + }); + } + + @BinderThread + @Override + public void updateWallpaperVisibility(int displayId, boolean visible) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.setWallpaperVisible(displayId, visible)); + } + + @BinderThread + @Override + public void checkNavBarModes(int displayId) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.checkNavBarModes(displayId)); + } + + @BinderThread + @Override + public void finishBarAnimations(int displayId) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.finishBarAnimations(displayId)); + } + + @BinderThread + @Override + public void touchAutoDim(int displayId, boolean reset) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.touchAutoDim(displayId, reset)); + } + + @BinderThread + @Override + public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode, + boolean animate) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.transitionTo(displayId, barMode, animate)); + } + + @BinderThread + @Override + public void appTransitionPending(boolean pending) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.appTransitionPending(pending)); } @Override @@ -359,6 +457,12 @@ public class TouchInteractionService extends Service { taskbarManager.onSystemBarAttributesChanged(displayId, behavior)); } + @Override + public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { + executeForTaskbarManager(taskbarManager -> + taskbarManager.onTransitionModeUpdated(barMode, checkBarModes)); + } + @Override public void onNavButtonsDarkIntensityChanged(float darkIntensity) { executeForTaskbarManager(taskbarManager -> @@ -371,6 +475,31 @@ public class TouchInteractionService extends Service { taskbarManager.onNavigationBarLumaSamplingEnabled(displayId, enable)); } + @Override + public void onUnbind(IRemoteCallback reply) { + // Run everything in the same main thread block to ensure the cleanup happens before + // sending the reply. + MAIN_EXECUTOR.execute(() -> { + executeForTaskbarManager(TaskbarManager::destroy); + try { + reply.sendResult(null); + } catch (RemoteException e) { + Log.w(TAG, "onUnbind: Failed to reply to LauncherProxyService", e); + } + }); + } + + @Override + public void onActionCornerActivated(int action, int displayId) { + MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> { + ActionCornerHandler actionCornerHandler = tis.mActionCornerHandler; + if (actionCornerHandler == null) { + return; + } + actionCornerHandler.handleAction(action, displayId); + })); + } + private void executeForTouchInteractionService( @NonNull Consumer tisConsumer) { TouchInteractionService tis = mTis.get(); @@ -423,7 +552,8 @@ public class TouchInteractionService extends Service { */ public void setPredictiveBackToHomeInProgress(boolean isInProgress) { executeForTouchInteractionService(tis -> - tis.mDeviceState.setPredictiveBackToHomeInProgress(isInProgress)); + tis.mDeviceStateRepository.forEach(/* createIfAbsent= */ true, deviceState -> + deviceState.setPredictiveBackToHomeInProgress(isInProgress))); } /** @@ -451,161 +581,237 @@ public class TouchInteractionService extends Service { */ public void setGestureBlockedTaskId(int taskId) { executeForTouchInteractionService( - tis -> tis.mDeviceState.setGestureBlockingTaskId(taskId)); - } - - /** Sets a listener to be run on Overview Target updates. */ - public void setOverviewTargetChangeListener(@Nullable Runnable listener) { - mOnOverviewTargetChangeListener = listener; - } - - protected void onOverviewTargetChange() { - if (mOnOverviewTargetChangeListener != null) { - mOnOverviewTargetChangeListener.run(); - mOnOverviewTargetChangeListener = null; - } + tis -> + tis.mDeviceStateRepository.forEach(/* createIfAbsent= */ true, + deviceState -> + deviceState.setGestureBlockingTaskId(taskId)) + ); } /** Refreshes the current overview target. */ - public void refreshOverviewTarget() { + @VisibleForTesting + public void refreshOverviewTargetForTest() { executeForTouchInteractionService(tis -> { tis.mAllAppsActionManager.onDestroy(); - tis.onOverviewTargetChange(tis.mOverviewComponentObserver.isHomeAndOverviewSame()); + tis.onOverviewTargetChanged(tis.mOverviewComponentObserver.isHomeAndOverviewSame()); + if (RecentsWindowFlags.getEnableOverviewInWindow()) { + Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedContext(); + if (launcher != null) { + tis.mTaskbarManager.setActivity(launcher); + } + } }); } } - private final InputManager.InputDeviceListener mInputDeviceListener = - new InputManager.InputDeviceListener() { - @Override - public void onInputDeviceAdded(int deviceId) { - if (isTrackpadDevice(deviceId)) { - boolean wasEmpty = mTrackpadsConnected.isEmpty(); - mTrackpadsConnected.add(deviceId); - if (wasEmpty) { - update(); - } - } - } - - @Override - public void onInputDeviceChanged(int deviceId) { - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - mTrackpadsConnected.remove(deviceId); - if (mTrackpadsConnected.isEmpty()) { - update(); - } - } - - private void update() { - if (mInputMonitorCompat != null && !mTrackpadsConnected.isEmpty()) { - // Don't destroy and reinitialize input monitor due to trackpad - // connecting when it's already set up. - return; - } - initInputMonitor("onTrackpadConnected()"); - } - - private boolean isTrackpadDevice(int deviceId) { - InputDevice inputDevice = mInputManager.getInputDevice(deviceId); - if (inputDevice == null) { - return false; - } - return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE - | InputDevice.SOURCE_TOUCHPAD); - } - }; - - private static boolean sConnected = false; - private static boolean sIsInitialized = false; - private RotationTouchHelper mRotationTouchHelper; - - public static boolean isConnected() { - return sConnected; - } - - public static boolean isInitialized() { - return sIsInitialized; - } + private PerDisplayRepository mRotationTouchHelperRepository; private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory = this::createLauncherSwipeHandler; private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory = this::createFallbackSwipeHandler; + private final AbsSwipeUpHandler.Factory mRecentsWindowSwipeHandlerFactory = + this::createRecentsWindowSwipeHandler; + // This needs to be a member to be queued and potentially removed later if the service is + // destroyed before the user is unlocked + private final Runnable mUserUnlockedRunnable = this::onUserUnlocked; private final ScreenOnTracker.ScreenOnListener mScreenOnListener = this::onScreenOnChanged; + private final OverviewChangeListener mOverviewChangeListener = this::onOverviewTargetChanged; private final TaskbarNavButtonCallbacks mNavCallbacks = new TaskbarNavButtonCallbacks() { @Override - public void onNavigateHome() { - mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME); + public void onNavigateHome(int displayId) { + if (enableOverviewOnConnectedDisplays()) { + mOverviewCommandHelper.addCommand(CommandType.HOME, displayId); + } else { + mOverviewCommandHelper.addCommand(CommandType.HOME, DEFAULT_DISPLAY); + } } @Override - public void onToggleOverview() { - mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_TOGGLE); + public void onToggleOverview(int displayId) { + if (enableOverviewOnConnectedDisplays()) { + mOverviewCommandHelper.addCommand(CommandType.TOGGLE, displayId); + } else { + mOverviewCommandHelper.addCommand(CommandType.TOGGLE, DEFAULT_DISPLAY); + } + } + + @Override + public void onHideOverview(int displayId) { + if (enableOverviewOnConnectedDisplays()) { + mOverviewCommandHelper.addCommand(CommandType.HIDE_ALT_TAB, displayId); + } else { + mOverviewCommandHelper.addCommand(CommandType.HIDE_ALT_TAB, DEFAULT_DISPLAY); + } } }; - private ActivityManagerWrapper mAM; + // We should clean up the recents window on the primary display on home intent start, however we + // have no other way of listening to this event in the 3P launcher case. + private final TaskStackChangeListener mHomeIntentStartedListener = + new TaskStackChangeListener() { + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + TaskStackChangeListener.super.onActivityRestartAttempt(task, homeTaskVisible, + clearedTask, wasVisible); + if (task.configuration.windowConfiguration.getActivityType() + != ACTIVITY_TYPE_HOME + || task.displayId != DEFAULT_DISPLAY) { + // We only want to handle home intent starts, and only on the primary + // display. + return; + } + if (mGestureState != DEFAULT_STATE) { + // If there's an ongoing gesture, we shouldn't clean up the recents window + // since gestures will clean up the recents window when needed. + return; + } + RecentsWindowManager recentsWindowManager = + mRecentsWindowManagerRepository.get(DEFAULT_DISPLAY); + TaskAnimationManager taskAnimationManager = + mTaskAnimationManagerRepository.get(DEFAULT_DISPLAY); + if (recentsWindowManager == null || taskAnimationManager == null) { + return; + } + if (taskAnimationManager.isRecentsAnimationRunning()) { + RecentsState recentsState = + recentsWindowManager.getStateManager().getState(); + if (!recentsState.isRecentsViewVisible()) { + // If we're in a state where the recents view is visible, we can ignore + // the recents animation running check, otherwise we should wait for + // the recents animation to end. + return; + } + } + if (recentsWindowManager.isStarted()) { + recentsWindowManager.getStateManager().goToState(RecentsState.HOME, true); + } + } + }; + private OverviewCommandHelper mOverviewCommandHelper; private OverviewComponentObserver mOverviewComponentObserver; private InputConsumerController mInputConsumer; - private RecentsAnimationDeviceState mDeviceState; - private TaskAnimationManager mTaskAnimationManager; + private PerDisplayRepository mDeviceStateRepository; + private PerDisplayRepository mTaskAnimationManagerRepository; - private @NonNull InputConsumer mUncheckedConsumer = InputConsumer.NO_OP; - private @NonNull InputConsumer mConsumer = InputConsumer.NO_OP; + private @NonNull InputConsumer mUncheckedConsumer = InputConsumer.DEFAULT_NO_OP; + + private @NonNull InputConsumer mConsumer = InputConsumer.DEFAULT_NO_OP; private Choreographer mMainChoreographer; - private @Nullable ResetGestureInputConsumer mResetGestureInputConsumer; + private boolean mUserUnlocked = false; private GestureState mGestureState = DEFAULT_STATE; + private InputMonitorDisplayModel mInputMonitorDisplayModel; private InputMonitorCompat mInputMonitorCompat; private InputEventReceiver mInputEventReceiver; private TaskbarManager mTaskbarManager; + private ActionCornerHandler mActionCornerHandler; private Function mSwipeUpProxyProvider = i -> null; private AllAppsActionManager mAllAppsActionManager; - private InputManager mInputManager; - private final Set mTrackpadsConnected = new ArraySet<>(); + private ActiveTrackpadList mTrackpadsConnected; + + private final SparseArray mGestureStartNavMode = new SparseArray<>(); + + private DesktopAppLaunchTransitionManager mDesktopAppLaunchTransitionManager; + + private DisplayController.DisplayInfoChangeListener mNavigationModeChangeListener; + private DisplayController.DisplayInfoChangeListener mNightModeChangeListener; + + PerDisplayRepository mRecentsWindowManagerRepository; + + private SystemDecorationChangeObserver mSystemDecorationChangeObserver; + + private DisplayRepository mDisplayRepository; + + private QuickstepKeyGestureEventsManager mQuickstepKeyGestureEventsHandler; + private DisplaysWithDecorationsRepositoryCompat mDisplaysWithDecorationsRepositoryCompat; + private CoroutineDispatcher mCoroutineDispatcher; @Override public void onCreate() { super.onCreate(); + Log.d(TAG, "onCreate: user=" + getUserId() + + " instance=" + System.identityHashCode(this)); // Initialize anything here that is needed in direct boot mode. // Everything else should be initialized in onUserUnlocked() below. + mDisplayRepository = LauncherDisplayRepository.getINSTANCE().get(this); + mDeviceStateRepository = RecentsAnimationDeviceState.REPOSITORY_INSTANCE.get(this); + mTaskAnimationManagerRepository = TaskAnimationManager.REPOSITORY_INSTANCE.get(this); mMainChoreographer = Choreographer.getInstance(); - mAM = ActivityManagerWrapper.getInstance(); - mDeviceState = new RecentsAnimationDeviceState(this, true); - mRotationTouchHelper = mDeviceState.getRotationTouchHelper(); - mAllAppsActionManager = new AllAppsActionManager( - this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent); - mInputManager = getSystemService(InputManager.class); - if (ENABLE_TRACKPAD_GESTURE.get()) { - mInputManager.registerInputDeviceListener(mInputDeviceListener, - UI_HELPER_EXECUTOR.getHandler()); - int [] inputDevices = mInputManager.getInputDeviceIds(); - for (int inputDeviceId : inputDevices) { - mInputDeviceListener.onInputDeviceAdded(inputDeviceId); + mRotationTouchHelperRepository = RotationTouchHelper.REPOSITORY_INSTANCE.get(this); + mRecentsWindowManagerRepository = RecentsWindowManager.REPOSITORY_INSTANCE.get(this); + mSystemDecorationChangeObserver = SystemDecorationChangeObserver.getINSTANCE().get(this); + mQuickstepKeyGestureEventsHandler = new QuickstepKeyGestureEventsManager(this); + mCoroutineDispatcher = ProductionDispatchers.INSTANCE.getMain(); + mDisplaysWithDecorationsRepositoryCompat = + LauncherDisplaysWithDecorationsRepositoryCompat.getINSTANCE().get(this); + mAllAppsActionManager = new AllAppsActionManager(this, UI_HELPER_EXECUTOR, + mQuickstepKeyGestureEventsHandler, + () -> mTaskbarManager.createAllAppsPendingIntent()); + mTrackpadsConnected = new ActiveTrackpadList(this, () -> { + if (mInputMonitorCompat != null && !mTrackpadsConnected.isEmpty()) { + // Don't destroy and reinitialize input monitor due to trackpad + // connecting when it's already set up. + return; } - } - mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks); + initInputMonitor("onTrackpadConnected()"); + }); + + mTaskbarManager = new TaskbarManagerImplWrapper( + new TaskbarManagerImpl(this, mAllAppsActionManager, mNavCallbacks, + mRecentsWindowManagerRepository, mDisplaysWithDecorationsRepositoryCompat, + mCoroutineDispatcher)); + mDesktopAppLaunchTransitionManager = + new DesktopAppLaunchTransitionManager(this, SystemUiProxy.INSTANCE.get(this)); + mDesktopAppLaunchTransitionManager.registerTransitions(); mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer(); // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized. - LockedUserState.get(this).runOnUserUnlocked(this::onUserUnlocked); - LockedUserState.get(this).runOnUserUnlocked(mTaskbarManager::onUserUnlocked); - mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged); - sConnected = true; - + LockedUserState.get(this).runOnUserUnlocked(mUserUnlockedRunnable); + // Assume that the navigation mode changes for all displays at once. + mNavigationModeChangeListener = + mDeviceStateRepository.get(DEFAULT_DISPLAY).addDisplayInfoChangeCallback( + CHANGE_NAVIGATION_MODE, this::onNavigationModeChanged); + // Assume that the night mode changes for all displays at once. + mNightModeChangeListener = + mDeviceStateRepository.get(DEFAULT_DISPLAY).addDisplayInfoChangeCallback( + CHANGE_NIGHT_MODE, this::onNightModeChanged); ScreenOnTracker.INSTANCE.get(this).addListener(mScreenOnListener); } + @Nullable + private InputEventReceiver getInputEventReceiver(int displayId) { + if (ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS.isTrue()) { + InputMonitorResource inputMonitorResource = mInputMonitorDisplayModel == null + ? null : mInputMonitorDisplayModel.getDisplayResource(displayId); + return inputMonitorResource == null ? null : inputMonitorResource.inputEventReceiver; + } + return mInputEventReceiver; + } + + @Nullable + private InputMonitorCompat getInputMonitorCompat(int displayId) { + if (ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS.isTrue()) { + InputMonitorResource inputMonitorResource = mInputMonitorDisplayModel == null + ? null : mInputMonitorDisplayModel.getDisplayResource(displayId); + return inputMonitorResource == null ? null : inputMonitorResource.inputMonitorCompat; + } + return mInputMonitorCompat; + } + private void disposeEventHandlers(String reason) { - Log.d(TAG, "disposeEventHandlers: Reason: " + reason); + Log.d(TAG, "disposeEventHandlers: Reason: " + reason + + " instance=" + System.identityHashCode(this)); + if (ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS.isTrue()) { + if (mInputMonitorDisplayModel == null) return; + mInputMonitorDisplayModel.destroy(); + return; + } if (mInputEventReceiver != null) { mInputEventReceiver.dispose(); mInputEventReceiver = null; @@ -618,17 +824,22 @@ public class TouchInteractionService extends Service { private void initInputMonitor(String reason) { disposeEventHandlers("Initializing input monitor due to: " + reason); - - if (mDeviceState.isButtonNavMode() && (!ENABLE_TRACKPAD_GESTURE.get() - || mTrackpadsConnected.isEmpty())) { + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(DEFAULT_DISPLAY); + if (deviceState.isButtonNavMode() + && !deviceState.supportsAssistantGestureInButtonNav() + && (mTrackpadsConnected.isEmpty())) { return; } + if (ENABLE_GESTURE_NAV_ON_CONNECTED_DISPLAYS.isTrue()) { + mInputMonitorDisplayModel = new InputMonitorDisplayModel( + this, mSystemDecorationChangeObserver); + } else { + mInputMonitorCompat = new InputMonitorCompat("swipe-up", DEFAULT_DISPLAY); + mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(), + mMainChoreographer, this::onInputEvent); + } - mInputMonitorCompat = new InputMonitorCompat("swipe-up", mDeviceState.getDisplayId()); - mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(), - mMainChoreographer, this::onInputEvent); - - mRotationTouchHelper.updateGestureTouchRegions(); + mRotationTouchHelperRepository.get(DEFAULT_DISPLAY).updateGestureTouchRegions(); } /** @@ -638,18 +849,25 @@ public class TouchInteractionService extends Service { initInputMonitor("onNavigationModeChanged()"); resetHomeBounceSeenOnQuickstepEnabledFirstTime(); } + private void onNightModeChanged() { + ActivityPreloadUtil.preloadOverviewForTIS(this, false /* fromInit */); + } @UiThread public void onUserUnlocked() { - Log.d(TAG, "onUserUnlocked: userId=" + getUserId()); - mTaskAnimationManager = new TaskAnimationManager(this); - mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState); + Log.d(TAG, "onUserUnlocked: userId=" + getUserId() + + " instance=" + System.identityHashCode(this)); + mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(this); mOverviewCommandHelper = new OverviewCommandHelper(this, - mOverviewComponentObserver, mTaskAnimationManager); - mResetGestureInputConsumer = new ResetGestureInputConsumer( - mTaskAnimationManager, mTaskbarManager::getCurrentActivityContext); + mOverviewComponentObserver, mDisplayRepository, mTaskbarManager, + mTaskAnimationManagerRepository); + mActionCornerHandler = LauncherComponentProvider.get( + this).getActionCornerHandlerFactory().create(mOverviewCommandHelper); + mUserUnlocked = true; mInputConsumer.registerInputConsumer(); - onSystemUiFlagsChanged(mDeviceState.getSystemUiStateFlags()); + mDeviceStateRepository.forEach(/* createIfAbsent= */ true, deviceState -> + onSystemUiFlagsChanged(deviceState.getSysuiStateFlags(), + deviceState.getDisplayId())); onAssistantVisibilityChanged(); // Initialize the task tracker @@ -659,8 +877,12 @@ public class TouchInteractionService extends Service { // new ModelPreload().start(this); resetHomeBounceSeenOnQuickstepEnabledFirstTime(); - mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange); - onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame()); + mOverviewComponentObserver.addOverviewChangeListener(mOverviewChangeListener); + onOverviewTargetChanged(mOverviewComponentObserver.isHomeAndOverviewSame()); + + mTaskbarManager.onUserUnlocked(); + mQuickstepKeyGestureEventsHandler.registerOverviewKeyGestureEvent( + createOverviewGestureHandler()); } public OverviewCommandHelper getOverviewCommandHelper() { @@ -668,7 +890,8 @@ public class TouchInteractionService extends Service { } private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { - if (!LockedUserState.get(this).isUserUnlocked() || mDeviceState.isButtonNavMode()) { + if (!LockedUserState.get(this).isUserUnlocked() || mDeviceStateRepository.get( + DEFAULT_DISPLAY).isButtonNavMode()) { // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation // mode doesn't have gestures return; @@ -683,83 +906,100 @@ public class TouchInteractionService extends Service { } } - private void onOverviewTargetChange(boolean isHomeAndOverviewSame) { + private void onOverviewTargetChanged(boolean isHomeAndOverviewSame) { mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame); - - StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface() - .getCreatedContainer(); - if (newOverviewActivity != null) { - mTaskbarManager.setActivity(newOverviewActivity); + RecentsViewContainer newOverviewContainer = + mOverviewComponentObserver.getContainerInterface( + DEFAULT_DISPLAY).getCreatedContainer(); + if (newOverviewContainer != null) { + if (newOverviewContainer instanceof StatefulActivity activity) { + // This will also call setRecentsViewContainer() internally. + mTaskbarManager.setActivity(activity); + } else { + mTaskbarManager.setRecentsViewContainer(newOverviewContainer); + } } - mTISBinder.onOverviewTargetChange(); - } - - private PendingIntent createAllAppsPendingIntent() { - if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) { - return new PendingIntent(new IIntentSender.Stub() { - @Override - public void send(int code, Intent intent, String resolvedType, - IBinder allowlistToken, IIntentReceiver finishedReceiver, - String requiredPermission, Bundle options) { - MAIN_EXECUTOR.execute(() -> mTaskbarManager.toggleAllApps()); - } - }); - } else { - return PendingIntent.getActivity( - this, - GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, - new Intent(mOverviewComponentObserver.getHomeIntent()) - .setAction(INTENT_ACTION_ALL_APPS_TOGGLE), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + if (RecentsWindowFlags.getEnableOverviewInWindow()) { + mRecentsWindowManagerRepository.forEach( + /* createIfAbsent= */ false, RecentsWindowManager::cleanupRecentsWindow); + if (isHomeAndOverviewSame) { + TaskStackChangeListeners.getInstance().unregisterTaskStackListener( + mHomeIntentStartedListener); + } else { + TaskStackChangeListeners.getInstance().registerTaskStackListener( + mHomeIntentStartedListener); + } } } @UiThread - private void onSystemUiFlagsChanged(@SystemUiStateFlags long lastSysUIFlags) { + private void onSystemUiFlagsChanged(@SystemUiStateFlags long lastSysUIFlags, int displayId) { if (LockedUserState.get(this).isUserUnlocked()) { - long systemUiStateFlags = mDeviceState.getSystemUiStateFlags(); - SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags); - mOverviewComponentObserver.onSystemUiStateChanged(); - mTaskbarManager.onSystemUiFlagsChanged(systemUiStateFlags); - mTaskAnimationManager.onSystemUiFlagsChanged(lastSysUIFlags, systemUiStateFlags); + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId); + TaskAnimationManager taskAnimationManager = mTaskAnimationManagerRepository.get( + displayId); + if (deviceState != null && taskAnimationManager != null) { + long systemUiStateFlags = deviceState.getSysuiStateFlags(); + mTaskbarManager.onSystemUiFlagsChanged(systemUiStateFlags, displayId); + if (displayId == DEFAULT_DISPLAY) { + // The following don't care about non-default displays, at least for now. If + // they + // ever will, they should be taken care of. + SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags); + mOverviewComponentObserver.setHomeDisabled(deviceState.isHomeDisabled()); + taskAnimationManager.onSystemUiFlagsChanged(lastSysUIFlags, systemUiStateFlags); + } + } } } @UiThread private void onAssistantVisibilityChanged() { if (LockedUserState.get(this).isUserUnlocked()) { - mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged( - mDeviceState.getAssistantVisibility()); + mOverviewComponentObserver.getContainerInterface( + DEFAULT_DISPLAY).onAssistantVisibilityChanged( + mDeviceStateRepository.get(DEFAULT_DISPLAY).getAssistantVisibility()); } } @Override public void onDestroy() { - Log.d(TAG, "Touch service destroyed: user=" + getUserId()); - sIsInitialized = false; + Log.d(TAG, "onDestroy: user=" + getUserId() + + " instance=" + System.identityHashCode(this)); if (LockedUserState.get(this).isUserUnlocked()) { mInputConsumer.unregisterInputConsumer(); - mOverviewComponentObserver.onDestroy(); + mQuickstepKeyGestureEventsHandler.onDestroy(); + mOverviewComponentObserver.setHomeDisabled(false); + mOverviewComponentObserver.removeOverviewChangeListener(mOverviewChangeListener); } disposeEventHandlers("TouchInteractionService onDestroy()"); - mDeviceState.destroy(); SystemUiProxy.INSTANCE.get(this).clearProxy(); mAllAppsActionManager.onDestroy(); - mInputManager.unregisterInputDeviceListener(mInputDeviceListener); - mTrackpadsConnected.clear(); - + mTrackpadsConnected.destroy(); mTaskbarManager.destroy(); - sConnected = false; - + if (mDesktopAppLaunchTransitionManager != null) { + mDesktopAppLaunchTransitionManager.unregisterTransitions(); + } + mDesktopAppLaunchTransitionManager = null; + mDeviceStateRepository.get(DEFAULT_DISPLAY).removeDisplayInfoChangeListener( + mNavigationModeChangeListener); + mDeviceStateRepository.get(DEFAULT_DISPLAY).removeDisplayInfoChangeListener( + mNightModeChangeListener); + LockedUserState.get(this).removeOnUserUnlockedRunnable(mUserUnlockedRunnable); ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener); + if (RecentsWindowFlags.getEnableOverviewInWindow()) { + TaskStackChangeListeners.getInstance().unregisterTaskStackListener( + mHomeIntentStartedListener); + } super.onDestroy(); } @Override public IBinder onBind(Intent intent) { - Log.d(TAG, "Touch service connected: user=" + getUserId()); + Log.d(TAG, "onBind: user=" + getUserId() + + " instance=" + System.identityHashCode(this)); return mTISBinder; } @@ -775,10 +1015,9 @@ public class TouchInteractionService extends Service { } private void onInputEvent(InputEvent ev) { + int displayId = ev.getDisplayId(); if (!(ev instanceof MotionEvent)) { - ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ") - .append("Cannot process input event: received unknown event ") - .append(ev.toString())); + ActiveGestureProtoLogProxy.logUnknownInputEvent(displayId, ev.toString()); return; } MotionEvent event = (MotionEvent) ev; @@ -786,14 +1025,33 @@ public class TouchInteractionService extends Service { TestLogging.recordMotionEvent( TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event); - boolean isUserUnlocked = LockedUserState.get(this).isUserUnlocked(); - if (!isUserUnlocked || (mDeviceState.isButtonNavMode() - && !isTrackpadMotionEvent(event))) { - ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ") - .append("Cannot process input event: ") - .append(!isUserUnlocked - ? "user is locked" - : "using 3-button nav and event is not a trackpad event")); + if (!LockedUserState.get(this).isUserUnlocked()) { + ActiveGestureProtoLogProxy.logOnInputEventUserLocked(displayId); + return; + } + + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId); + if (deviceState == null) { + Log.d(TAG, "RecentsAnimationDeviceState not available for displayId " + displayId); + return; + } + + RotationTouchHelper rotationTouchHelper = mRotationTouchHelperRepository.get(displayId); + if (rotationTouchHelper == null) { + Log.d(TAG, "RotationTouchHelper not available for displayId " + displayId); + return; + } + + NavigationMode currentNavMode = deviceState.getMode(); + NavigationMode gestureStartNavMode = mGestureStartNavMode.get(displayId); + if (gestureStartNavMode != null && gestureStartNavMode != currentNavMode) { + ActiveGestureProtoLogProxy.logOnInputEventNavModeSwitched( + displayId, gestureStartNavMode.name(), currentNavMode.name()); + event.setAction(ACTION_CANCEL); + } else if (deviceState.isButtonNavMode() + && !deviceState.supportsAssistantGestureInButtonNav() + && !isTrackpadMotionEvent(event)) { + ActiveGestureProtoLogProxy.logOnInputEventThreeButtonNav(displayId); return; } @@ -803,119 +1061,154 @@ public class TouchInteractionService extends Service { boolean isHoverActionWithoutConsumer = enableCursorHoverStates() && isHoverActionWithoutConsumer(event); - if (enableHandleDelayedGestureCallbacks()) { + TaskAnimationManager taskAnimationManager = mTaskAnimationManagerRepository.get(displayId); + if (taskAnimationManager == null) { + Log.e(TAG, "TaskAnimationManager not available for displayId " + displayId); + ActiveGestureProtoLogProxy.logOnTaskAnimationManagerNotAvailable(displayId); + return; + } + if (action == ACTION_DOWN || isHoverActionWithoutConsumer) { + taskAnimationManager.notifyNewGestureStart(); + } + if (taskAnimationManager.shouldIgnoreMotionEvents()) { if (action == ACTION_DOWN || isHoverActionWithoutConsumer) { - mTaskAnimationManager.notifyNewGestureStart(); - } - if (mTaskAnimationManager.shouldIgnoreMotionEvents()) { - if (action == ACTION_DOWN || isHoverActionWithoutConsumer) { - ActiveGestureLog.INSTANCE.addLog( - new CompoundString("TIS.onMotionEvent: A new gesture has been ") - .append("started, but a previously-requested recents ") - .append("animation hasn't started. Ignoring all following ") - .append("motion events."), - RECENTS_ANIMATION_START_PENDING); - } - return; + ActiveGestureProtoLogProxy.logOnInputIgnoringFollowingEvents(displayId); } + return; + } + + InputMonitorCompat inputMonitorCompat = getInputMonitorCompat(displayId); + InputEventReceiver inputEventReceiver = getInputEventReceiver(displayId); + + if (action == ACTION_DOWN || isHoverActionWithoutConsumer) { + mGestureStartNavMode.set(displayId, currentNavMode); + } else if (action == ACTION_UP || action == ACTION_CANCEL) { + mGestureStartNavMode.delete(displayId); } SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent"); CompoundString reasonString = action == ACTION_DOWN - ? new CompoundString("TIS.onMotionEvent: ") : CompoundString.NO_OP; + ? CompoundString.newEmptyString() : CompoundString.NO_OP; if (action == ACTION_DOWN || isHoverActionWithoutConsumer) { - mRotationTouchHelper.setOrientationTransformIfNeeded(event); + rotationTouchHelper.setOrientationTransformIfNeeded(event); - boolean isOneHandedModeActive = mDeviceState.isOneHandedModeActive(); - boolean isInSwipeUpTouchRegion = mRotationTouchHelper.isInSwipeUpTouchRegion(event); + boolean isOneHandedModeActive = deviceState.isOneHandedModeActive(); + boolean isInSwipeUpTouchRegion = rotationTouchHelper.isInSwipeUpTouchRegion(event); TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext(); - if (isInSwipeUpTouchRegion && tac != null) { - tac.closeKeyboardQuickSwitchView(); - } - if ((!isOneHandedModeActive && isInSwipeUpTouchRegion) - || isHoverActionWithoutConsumer) { + BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null; + boolean isOnBubbles = bubbleControllers != null + && BubbleBarInputConsumer.isEventOnBubbles(tac, event); + if (deviceState.isButtonNavMode() + && deviceState.supportsAssistantGestureInButtonNav()) { + reasonString.append("in three button mode which supports Assistant gesture"); + // Consume gesture event for Assistant (all other gestures should do nothing). + if (deviceState.canTriggerAssistantAction(event)) { + reasonString.append(" and event can trigger assistant action, " + + "consuming gesture for assistant action"); + mGestureState = createGestureState( + displayId, mGestureState, getTrackpadGestureType(event)); + mUncheckedConsumer = tryCreateAssistantInputConsumer( + this, + deviceState, + inputMonitorCompat, + mGestureState, + event); + } else { + reasonString.append(" but event cannot trigger Assistant, " + + "consuming gesture as no-op"); + mUncheckedConsumer = createNoOpInputConsumer(displayId); + } + } else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion) + || isHoverActionWithoutConsumer || isOnBubbles) { reasonString.append(!isOneHandedModeActive && isInSwipeUpTouchRegion - ? "one handed mode is not active and event is in swipe up region" - : "isHoverActionWithoutConsumer == true") - .append(", creating new input consumer"); + ? "one handed mode is not active and event is in swipe up region, " + + "creating new input consumer" + : "isHoverActionWithoutConsumer == true, creating new input consumer"); // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger // onConsumerInactive and wipe the previous gesture state GestureState prevGestureState = new GestureState(mGestureState); - GestureState newGestureState = createGestureState(mGestureState, - getTrackpadGestureType(event)); + GestureState newGestureState = createGestureState( + displayId, mGestureState, getTrackpadGestureType(event)); mConsumer.onConsumerAboutToBeSwitched(); mGestureState = newGestureState; - mConsumer = newConsumer(prevGestureState, mGestureState, event); + mConsumer = newConsumer( + this, + mUserUnlocked, + mOverviewComponentObserver, + deviceState, + prevGestureState, + mGestureState, + taskAnimationManager, + inputMonitorCompat, + getSwipeUpHandlerFactory(displayId), + this::onConsumerInactive, + inputEventReceiver, + mTaskbarManager, + mSwipeUpProxyProvider, + mOverviewCommandHelper, + event, + rotationTouchHelper); mUncheckedConsumer = mConsumer; - } else if ((mDeviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event)) - && mDeviceState.canTriggerAssistantAction(event)) { - reasonString.append(mDeviceState.isFullyGesturalNavMode() - ? "using fully gestural nav" - : "event is a trackpad multi-finger swipe") - .append(" and event can trigger assistant action") - .append(", consuming gesture for assistant action"); - mGestureState = createGestureState(mGestureState, - getTrackpadGestureType(event)); + } else if ((deviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event)) + && deviceState.canTriggerAssistantAction(event)) { + reasonString.append(deviceState.isFullyGesturalNavMode() + ? "using fully gestural nav and event can trigger assistant action, " + + "consuming gesture for assistant action" + : "event is a trackpad multi-finger swipe and event can trigger assistant " + + "action, consuming gesture for assistant action"); + mGestureState = createGestureState( + displayId, mGestureState, getTrackpadGestureType(event)); // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we // should not interrupt it. QuickSwitch assumes that interruption can only // happen if the next gesture is also quick switch. - mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event); - } else if (mDeviceState.canTriggerOneHandedAction(event)) { - reasonString.append("event can trigger one-handed action") - .append(", consuming gesture for one-handed action"); + mUncheckedConsumer = tryCreateAssistantInputConsumer( + this, deviceState, inputMonitorCompat, mGestureState, event); + } else if (deviceState.canTriggerOneHandedAction(event)) { + reasonString.append("event can trigger one-handed action, " + + "consuming gesture for one-handed action"); // Consume gesture event for triggering one handed feature. - mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState, - InputConsumer.NO_OP, mInputMonitorCompat); + mUncheckedConsumer = new OneHandedModeInputConsumer( + this, + displayId, + deviceState, + InputConsumer.createNoOpInputConsumer(displayId), inputMonitorCompat); } else { - mUncheckedConsumer = InputConsumer.NO_OP; + mUncheckedConsumer = InputConsumer.createNoOpInputConsumer(displayId); } } else { // Other events - if (mUncheckedConsumer != InputConsumer.NO_OP) { + if (mUncheckedConsumer.getType() != InputConsumer.TYPE_NO_OP) { // Only transform the event if we are handling it in a proper consumer - mRotationTouchHelper.setOrientationTransformIfNeeded(event); + rotationTouchHelper.setOrientationTransformIfNeeded(event); } } - if (mUncheckedConsumer != InputConsumer.NO_OP) { + if (mUncheckedConsumer.getType() != InputConsumer.TYPE_NO_OP) { switch (action) { case ACTION_DOWN: - ActiveGestureLog.INSTANCE.addLog(reasonString); + ActiveGestureProtoLogProxy.logOnInputEventActionDown(displayId, reasonString); // fall through case ACTION_UP: - ActiveGestureLog.INSTANCE.addLog( - new CompoundString("onMotionEvent(") - .append((int) event.getRawX()) - .append(", ") - .append((int) event.getRawY()) - .append("): ") - .append(MotionEvent.actionToString(action)) - .append(", ") - .append(MotionEvent.classificationToString( - event.getClassification())), - /* gestureEvent= */ action == ACTION_DOWN - ? MOTION_DOWN - : MOTION_UP); + ActiveGestureProtoLogProxy.logOnInputEventActionUp( + (int) event.getRawX(), + (int) event.getRawY(), + action, + MotionEvent.classificationToString(event.getClassification()), + displayId); break; case ACTION_MOVE: - ActiveGestureLog.INSTANCE.addLog( - new CompoundString("onMotionEvent: ") - .append(MotionEvent.actionToString(action)) - .append(",") - .append(MotionEvent.classificationToString( - event.getClassification())) - .append(", pointerCount: ") - .append(event.getPointerCount()), - MOTION_MOVE); + ActiveGestureProtoLogProxy.logOnInputEventActionMove( + MotionEvent.actionToString(action), + MotionEvent.classificationToString(event.getClassification()), + event.getPointerCount(), + displayId); break; default: { - ActiveGestureLog.INSTANCE.addLog( - new CompoundString("onMotionEvent: ") - .append(MotionEvent.actionToString(action)) - .append(",") - .append(MotionEvent.classificationToString( - event.getClassification()))); + ActiveGestureProtoLogProxy.logOnInputEventGenericAction( + MotionEvent.actionToString(action), + MotionEvent.classificationToString(event.getClassification()), + displayId); } } } @@ -939,14 +1232,15 @@ public class TouchInteractionService extends Service { } if (cleanUpConsumer) { - reset(); + reset(displayId); } traceToken.close(); } private boolean isHoverActionWithoutConsumer(MotionEvent event) { // Only process these events when taskbar is present. - TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext(); + int displayId = event.getDisplayId(); + TaskbarActivityContext tac = mTaskbarManager.getTaskbarForDisplay(displayId); boolean isTaskbarPresent = tac != null && tac.getDeviceProfile().isTaskbarPresent && !tac.isPhoneMode(); return event.isHoverEvent() && (mUncheckedConsumer.getType() & TYPE_CURSOR_HOVER) == 0 @@ -958,441 +1252,57 @@ public class TouchInteractionService extends Service { return event.isHoverEvent() && event.getSource() == InputDevice.SOURCE_MOUSE; } - private InputConsumer tryCreateAssistantInputConsumer( - GestureState gestureState, MotionEvent motionEvent) { - return tryCreateAssistantInputConsumer( - InputConsumer.NO_OP, gestureState, motionEvent, CompoundString.NO_OP); - } - - private InputConsumer tryCreateAssistantInputConsumer( - InputConsumer base, - GestureState gestureState, - MotionEvent motionEvent, - CompoundString reasonString) { - if (mDeviceState.isGestureBlockedTask(gestureState.getRunningTask())) { - reasonString.append(SUBSTRING_PREFIX) - .append("is gesture-blocked task, using base input consumer"); - return base; - } else { - reasonString.append(SUBSTRING_PREFIX).append("using AssistantInputConsumer"); - return new AssistantInputConsumer( - this, gestureState, base, mInputMonitorCompat, mDeviceState, motionEvent); - } - } - - public GestureState createGestureState(GestureState previousGestureState, - GestureState.TrackpadGestureType trackpadGestureType) { + public GestureState createGestureState( + int displayId, + GestureState previousGestureState, + GestureState.TrackpadGestureType trackpadGestureType) { final GestureState gestureState; TopTaskTracker.CachedTaskInfo taskInfo; - if (mTaskAnimationManager.isRecentsAnimationRunning()) { - gestureState = new GestureState(mOverviewComponentObserver, - ActiveGestureLog.INSTANCE.getLogId()); + TaskAnimationManager taskAnimationManager = mTaskAnimationManagerRepository.get(displayId); + if (taskAnimationManager != null && taskAnimationManager.isRecentsAnimationRunning()) { + gestureState = new GestureState( + mOverviewComponentObserver, displayId, ActiveGestureLog.INSTANCE.getLogId()); TopTaskTracker.CachedTaskInfo previousTaskInfo = previousGestureState.getRunningTask(); // previousTaskInfo can be null iff previousGestureState == GestureState.DEFAULT_STATE taskInfo = previousTaskInfo != null ? previousTaskInfo - : TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false); + : TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false, displayId); gestureState.updateRunningTask(taskInfo); gestureState.updateLastStartedTaskIds(previousGestureState.getLastStartedTaskIds()); gestureState.updatePreviouslyAppearedTaskIds( previousGestureState.getPreviouslyAppearedTaskIds()); } else { - gestureState = new GestureState(mOverviewComponentObserver, + gestureState = new GestureState( + mOverviewComponentObserver, + displayId, ActiveGestureLog.INSTANCE.incrementLogId()); - taskInfo = TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false); + taskInfo = TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false, displayId); gestureState.updateRunningTask(taskInfo); } gestureState.setTrackpadGestureType(trackpadGestureType); // Log initial state for the gesture. - ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current running task package name=") - .append(taskInfo.getPackageName())); - ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current SystemUi state flags=") - .append(mDeviceState.getSystemUiStateString())); + ActiveGestureProtoLogProxy.logRunningTaskPackage(taskInfo.getPackageName()); + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId); + if (deviceState != null) { + ActiveGestureProtoLogProxy.logSysuiStateFlags(deviceState.getSystemUiStateString()); + } return gestureState; } - private InputConsumer newConsumer( - GestureState previousGestureState, GestureState newGestureState, MotionEvent event) { - TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext(); - AnimatedFloat progressProxy = mSwipeUpProxyProvider.apply(mGestureState); - if (progressProxy != null) { - InputConsumer consumer = new ProgressDelegateInputConsumer( - this, mTaskAnimationManager, mGestureState, mInputMonitorCompat, progressProxy); - - logInputConsumerSelectionReason(consumer, newCompoundString( - "mSwipeUpProxyProvider has been set, using ProgressDelegateInputConsumer")); - - return consumer; - } - - boolean canStartSystemGesture = - mGestureState.isTrackpadGesture() ? mDeviceState.canStartTrackpadGesture() - : mDeviceState.canStartSystemGesture(); - - if (!LockedUserState.get(this).isUserUnlocked()) { - CompoundString reasonString = newCompoundString("device locked"); - InputConsumer consumer; - if (canStartSystemGesture) { - // This handles apps launched in direct boot mode (e.g. dialer) as well as apps - // launched while device is locked even after exiting direct boot mode (e.g. camera). - consumer = createDeviceLockedInputConsumer( - newGestureState, reasonString.append(SUBSTRING_PREFIX) - .append("can start system gesture")); - } else { - consumer = getDefaultInputConsumer( - reasonString.append(SUBSTRING_PREFIX) - .append("cannot start system gesture")); - } - logInputConsumerSelectionReason(consumer, reasonString); - return consumer; - } - - CompoundString reasonString; - InputConsumer base; - // When there is an existing recents animation running, bypass systemState check as this is - // a followup gesture and the first gesture started in a valid system state. - if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning()) { - reasonString = newCompoundString(canStartSystemGesture - ? "can start system gesture" : "recents animation was running") - .append(", trying to use base consumer"); - base = newBaseConsumer(previousGestureState, newGestureState, event, reasonString); + /** + * Returns a AbsSwipeUpHandler.Factory, used to instantiate AbsSwipeUpHandler later. + * @param displayId The displayId of the display this handler will be used on. + */ + public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory(int displayId) { + BaseContainerInterface containerInterface = + mOverviewComponentObserver.getContainerInterface(displayId); + if (containerInterface instanceof FallbackWindowInterface) { + return mRecentsWindowSwipeHandlerFactory; + } else if (containerInterface instanceof LauncherActivityInterface) { + return mLauncherSwipeHandlerFactory; } else { - reasonString = newCompoundString( - "cannot start system gesture and recents animation was not running") - .append(", trying to use default input consumer"); - base = getDefaultInputConsumer(reasonString); - } - if (mDeviceState.isGesturalNavMode() || newGestureState.isTrackpadGesture()) { - handleOrientationSetup(base); - } - if (mDeviceState.isFullyGesturalNavMode() || newGestureState.isTrackpadGesture()) { - String reasonPrefix = - "device is in gesture navigation mode or 3-button mode with a trackpad gesture"; - if (mDeviceState.canTriggerAssistantAction(event)) { - reasonString.append(NEWLINE_PREFIX) - .append(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("gesture can trigger the assistant") - .append(", trying to use assistant input consumer"); - base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString); - } - - // If Taskbar is present, we listen for swipe or cursor hover events to unstash it. - if (tac != null && !(base instanceof AssistantInputConsumer)) { - // Present always on large screen or on small screen w/ flag - boolean useTaskbarConsumer = tac.getDeviceProfile().isTaskbarPresent - && !tac.isPhoneMode() - && !tac.isInStashedLauncherState(); - if (canStartSystemGesture && useTaskbarConsumer) { - reasonString.append(NEWLINE_PREFIX) - .append(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("TaskbarActivityContext != null, ") - .append("using TaskbarUnstashInputConsumer"); - base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac, - mOverviewCommandHelper, mGestureState); - } - } - if (enableBubblesLongPressNavHandle()) { - // Create bubbles input consumer before NavHandleLongPressInputConsumer. - // This allows for nav handle to fall back to bubbles. - if (mDeviceState.isBubblesExpanded()) { - reasonString = newCompoundString(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("bubbles expanded, trying to use default input consumer"); - // Bubbles can handle home gesture itself. - base = getDefaultInputConsumer(reasonString); - } - } - - NavHandle navHandle = tac != null ? tac.getNavHandle() - : SystemUiProxy.INSTANCE.get(this); - if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning() - && navHandle.canNavHandleBeLongPressed() - && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) { - reasonString.append(NEWLINE_PREFIX) - .append(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("Not running recents animation, "); - if (tac != null && tac.getNavHandle().canNavHandleBeLongPressed()) { - reasonString.append("stashed handle is long-pressable, "); - } - reasonString.append("using NavHandleLongPressInputConsumer"); - base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat, - mDeviceState, navHandle, mGestureState); - } - - if (!enableBubblesLongPressNavHandle()) { - // Continue overriding nav handle input consumer with bubbles - if (mDeviceState.isBubblesExpanded()) { - reasonString = newCompoundString(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("bubbles expanded, trying to use default input consumer"); - // Bubbles can handle home gesture itself. - base = getDefaultInputConsumer(reasonString); - } - } - - if (mDeviceState.isSystemUiDialogShowing()) { - reasonString = newCompoundString(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("system dialog is showing, using SysUiOverlayInputConsumer"); - base = new SysUiOverlayInputConsumer( - getBaseContext(), mDeviceState, mInputMonitorCompat); - } - - if (mGestureState.isTrackpadGesture() - && canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()) { - reasonString = newCompoundString(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("Trackpad 3-finger gesture, using TrackpadStatusBarInputConsumer"); - base = new TrackpadStatusBarInputConsumer(getBaseContext(), base, - mInputMonitorCompat); - } - - if (mDeviceState.isScreenPinningActive()) { - reasonString = newCompoundString(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("screen pinning is active, using ScreenPinnedInputConsumer"); - // Note: we only allow accessibility to wrap this, and it replaces the previous - // base input consumer (which should be NO_OP anyway since topTaskLocked == true). - base = new ScreenPinnedInputConsumer(this, newGestureState); - } - - if (mDeviceState.canTriggerOneHandedAction(event)) { - reasonString.append(NEWLINE_PREFIX) - .append(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("gesture can trigger one handed mode") - .append(", using OneHandedModeInputConsumer"); - base = new OneHandedModeInputConsumer( - this, mDeviceState, base, mInputMonitorCompat); - } - - if (mDeviceState.isAccessibilityMenuAvailable()) { - reasonString.append(NEWLINE_PREFIX) - .append(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("accessibility menu is available") - .append(", using AccessibilityInputConsumer"); - base = new AccessibilityInputConsumer( - this, mDeviceState, mGestureState, base, mInputMonitorCompat); - } - } else { - String reasonPrefix = "device is not in gesture navigation mode"; - if (mDeviceState.isScreenPinningActive()) { - reasonString = newCompoundString(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("screen pinning is active, trying to use default input consumer"); - base = getDefaultInputConsumer(reasonString); - } - - if (mDeviceState.canTriggerOneHandedAction(event)) { - reasonString.append(NEWLINE_PREFIX) - .append(reasonPrefix) - .append(SUBSTRING_PREFIX) - .append("gesture can trigger one handed mode") - .append(", using OneHandedModeInputConsumer"); - base = new OneHandedModeInputConsumer( - this, mDeviceState, base, mInputMonitorCompat); - } - } - logInputConsumerSelectionReason(base, reasonString); - return base; - } - - private CompoundString newCompoundString(String substring) { - return new CompoundString(NEWLINE_PREFIX).append(substring); - } - - private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) { - return gestureState.isThreeFingerTrackpadGesture(); - } - - private void logInputConsumerSelectionReason( - InputConsumer consumer, CompoundString reasonString) { - ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ") - .append(consumer.getName()) - .append(". reason(s):") - .append(reasonString)); - if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) { - ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER); - } - } - - private void handleOrientationSetup(InputConsumer baseInputConsumer) { - baseInputConsumer.notifyOrientationSetup(); - } - - private InputConsumer newBaseConsumer( - GestureState previousGestureState, - GestureState gestureState, - MotionEvent event, - CompoundString reasonString) { - if (mDeviceState.isKeyguardShowingOccluded()) { - // This handles apps showing over the lockscreen (e.g. camera) - return createDeviceLockedInputConsumer( - gestureState, - reasonString.append(SUBSTRING_PREFIX) - .append("keyguard is showing occluded") - .append(", trying to use device locked input consumer")); - } - - reasonString.append(SUBSTRING_PREFIX).append("keyguard is not showing occluded"); - - TopTaskTracker.CachedTaskInfo runningTask = gestureState.getRunningTask(); - // Use overview input consumer for sharesheets on top of home. - boolean forceOverviewInputConsumer = gestureState.getContainerInterface().isStarted() - && runningTask != null - && runningTask.isRootChooseActivity(); - - // In the case where we are in an excluded, translucent overlay, ignore it and treat the - // running activity as the task behind the overlay. - TopTaskTracker.CachedTaskInfo otherVisibleTask = runningTask == null - ? null - : runningTask.getVisibleNonExcludedTask(); - if (otherVisibleTask != null) { - ActiveGestureLog.INSTANCE.addLog(new CompoundString("Changing active task to ") - .append(otherVisibleTask.getPackageName()) - .append(" because the previous task running on top of this one (") - .append(runningTask.getPackageName()) - .append(") was excluded from recents")); - gestureState.updateRunningTask(otherVisibleTask); - } - - boolean previousGestureAnimatedToLauncher = - previousGestureState.isRunningAnimationToLauncher() - || mDeviceState.isPredictiveBackToHomeInProgress(); - // with shell-transitions, home is resumed during recents animation, so - // explicitly check against recents animation too. - boolean launcherResumedThroughShellTransition = - gestureState.getContainerInterface().isResumed() - && !previousGestureState.isRecentsAnimationRunning(); - // If a task fragment within Launcher is resumed - boolean launcherChildActivityResumed = useActivityOverlay() - && runningTask != null - && runningTask.isHomeTask() - && mOverviewComponentObserver.isHomeAndOverviewSame() - && !launcherResumedThroughShellTransition - && !previousGestureState.isRecentsAnimationRunning(); - - if (gestureState.getContainerInterface().isInLiveTileMode()) { - return createOverviewInputConsumer( - previousGestureState, - gestureState, - event, - forceOverviewInputConsumer, - reasonString.append(SUBSTRING_PREFIX) - .append("is in live tile mode, trying to use overview input consumer")); - } else if (runningTask == null) { - return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX) - .append("running task == null")); - } else if (previousGestureAnimatedToLauncher - || launcherResumedThroughShellTransition - || forceOverviewInputConsumer) { - return createOverviewInputConsumer( - previousGestureState, - gestureState, - event, - forceOverviewInputConsumer, - reasonString.append(SUBSTRING_PREFIX) - .append(previousGestureAnimatedToLauncher - ? "previous gesture animated to launcher" - : (launcherResumedThroughShellTransition - ? "launcher resumed through a shell transition" - : "forceOverviewInputConsumer == true")) - .append(", trying to use overview input consumer")); - } else if (mDeviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) { - return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX) - .append(launcherChildActivityResumed - ? "is launcher child-task, trying to use default input consumer" - : "is gesture-blocked task, trying to use default input consumer")); - } else { - reasonString.append(SUBSTRING_PREFIX) - .append("using OtherActivityInputConsumer"); - return createOtherActivityInputConsumer(gestureState, event); - } - } - - public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() { - return !mOverviewComponentObserver.isHomeAndOverviewSame() - ? mFallbackSwipeHandlerFactory : mLauncherSwipeHandlerFactory; - } - - private InputConsumer createOtherActivityInputConsumer(GestureState gestureState, - MotionEvent event) { - - final AbsSwipeUpHandler.Factory factory = getSwipeUpHandlerFactory(); - final boolean shouldDefer = !mOverviewComponentObserver.isHomeAndOverviewSame() - || gestureState.getContainerInterface().deferStartingActivity(mDeviceState, event); - final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event); - return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager, - gestureState, shouldDefer, this::onConsumerInactive, - mInputMonitorCompat, mInputEventReceiver, disableHorizontalSwipe, factory); - } - - private InputConsumer createDeviceLockedInputConsumer( - GestureState gestureState, CompoundString reasonString) { - if ((mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture()) - && gestureState.getRunningTask() != null) { - reasonString.append(SUBSTRING_PREFIX) - .append("device is in gesture nav mode or 3-button mode with a trackpad") - .append(" gesture and running task != null") - .append(", using DeviceLockedInputConsumer"); - return new DeviceLockedInputConsumer( - this, mDeviceState, mTaskAnimationManager, gestureState, mInputMonitorCompat); - } else { - return getDefaultInputConsumer(reasonString - .append(SUBSTRING_PREFIX) - .append((mDeviceState.isFullyGesturalNavMode() - || gestureState.isTrackpadGesture()) - ? "running task == null" - : "device is not in gesture nav mode and it's not a trackpad gesture") - .append(", trying to use default input consumer")); - } - } - - public InputConsumer createOverviewInputConsumer( - GestureState previousGestureState, - GestureState gestureState, - MotionEvent event, - boolean forceOverviewInputConsumer, - CompoundString reasonString) { - RecentsViewContainer container = gestureState.getContainerInterface().getCreatedContainer(); - if (container == null) { - return getDefaultInputConsumer( - reasonString.append(SUBSTRING_PREFIX) - .append("activity == null, trying to use default input consumer")); - } - - boolean hasWindowFocus = container.getRootView().hasWindowFocus(); - boolean isPreviousGestureAnimatingToLauncher = - previousGestureState.isRunningAnimationToLauncher() - || mDeviceState.isPredictiveBackToHomeInProgress(); - boolean isInLiveTileMode = gestureState.getContainerInterface().isInLiveTileMode(); - - reasonString.append(SUBSTRING_PREFIX) - .append(hasWindowFocus - ? "activity has window focus" - : (isPreviousGestureAnimatingToLauncher - ? "previous gesture is still animating to launcher" - : isInLiveTileMode - ? "device is in live mode" - : "all overview focus conditions failed")); - if (hasWindowFocus - || isPreviousGestureAnimatingToLauncher - || isInLiveTileMode) { - reasonString.append(SUBSTRING_PREFIX) - .append("overview should have focus, using OverviewInputConsumer"); - return new OverviewInputConsumer(gestureState, container, mInputMonitorCompat, - false /* startingInActivityBounds */); - } else { - reasonString.append(SUBSTRING_PREFIX).append( - "overview shouldn't have focus, using OverviewWithoutFocusInputConsumer"); - final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event); - return new OverviewWithoutFocusInputConsumer(container.asContext(), mDeviceState, - gestureState, mInputMonitorCompat, disableHorizontalSwipe); + return mFallbackSwipeHandlerFactory; } } @@ -1403,111 +1313,57 @@ public class TouchInteractionService extends Service { */ private void onConsumerInactive(InputConsumer caller) { if (mConsumer != null && mConsumer.getActiveConsumerInHierarchy() == caller) { - reset(); + reset(caller.getDisplayId()); } } - private void reset() { - mConsumer = mUncheckedConsumer = getDefaultInputConsumer(); + private void reset(int displayId) { + mConsumer = mUncheckedConsumer = InputConsumerUtils.getDefaultInputConsumer( + displayId, + mUserUnlocked, + mTaskAnimationManagerRepository.get(displayId), + mTaskbarManager, + CompoundString.NO_OP); mGestureState = DEFAULT_STATE; // By default, use batching of the input events, but check receiver before using in the rare // case that the monitor was disposed before the swipe settled - if (mInputEventReceiver != null) { - mInputEventReceiver.setBatchingEnabled(true); + InputEventReceiver inputEventReceiver = getInputEventReceiver(displayId); + if (inputEventReceiver != null) { + inputEventReceiver.setBatchingEnabled(true); } } - private @NonNull InputConsumer getDefaultInputConsumer() { - return getDefaultInputConsumer(CompoundString.NO_OP); - } - - /** - * Returns the {@link ResetGestureInputConsumer} if user is unlocked, else NO_OP. - */ - private @NonNull InputConsumer getDefaultInputConsumer(@NonNull CompoundString reasonString) { - if (mResetGestureInputConsumer != null) { - reasonString.append(SUBSTRING_PREFIX).append( - "mResetGestureInputConsumer initialized, using ResetGestureInputConsumer"); - return mResetGestureInputConsumer; - } else { - reasonString.append(SUBSTRING_PREFIX).append( - "mResetGestureInputConsumer not initialized, using no-op input consumer"); - // mResetGestureInputConsumer isn't initialized until onUserUnlocked(), so reset to - // NO_OP until then (we never want these to be null). - return InputConsumer.NO_OP; - } - } - - private void preloadOverview(boolean fromInit) { - Trace.beginSection("preloadOverview(fromInit=" + fromInit + ")"); - preloadOverview(fromInit, false); - Trace.endSection(); - } - - private void preloadOverview(boolean fromInit, boolean forSUWAllSet) { - if (!LockedUserState.get(this).isUserUnlocked()) { - return; - } - - if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) { - // Prevent the overview from being started before the real home on first boot. - return; - } - - if ((RestoreDbTask.isPending(this) && !forSUWAllSet) - || !mDeviceState.isUserSetupComplete()) { - // Preloading while a restore is pending may cause launcher to start the restore - // too early. - return; - } - - final BaseActivityInterface activityInterface = - mOverviewComponentObserver.getActivityInterface(); - final Intent overviewIntent = new Intent( - mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState()); - if (activityInterface.getCreatedContainer() != null && fromInit) { - // The activity has been created before the initialization of overview service. It is - // usually happens when booting or launcher is the top activity, so we should already - // have the latest state. - return; - } - - // TODO(b/258022658): Remove temporary logging. - Log.i(TAG, "preloadOverview: forSUWAllSet=" + forSUWAllSet - + ", isHomeAndOverviewSame=" + mOverviewComponentObserver.isHomeAndOverviewSame()); - - ActiveGestureLog.INSTANCE.addLog("preloadRecentsAnimation"); - mTaskAnimationManager.preloadRecentsAnimation(overviewIntent); - } - @Override public void onConfigurationChanged(Configuration newConfig) { if (!LockedUserState.get(this).isUserUnlocked()) { return; } - final BaseActivityInterface activityInterface = - mOverviewComponentObserver.getActivityInterface(); - final BaseDraggingActivity activity = activityInterface.getCreatedContainer(); - if (activity == null || activity.isStarted()) { + // TODO (b/399094853): handle config updates for all connected displays (relevant only for + // gestures on external displays) + final BaseContainerInterface containerInterface = + mOverviewComponentObserver.getContainerInterface(DEFAULT_DISPLAY); + final RecentsViewContainer container = containerInterface.getCreatedContainer(); + if (container == null || container.isStarted()) { // We only care about the existing background activity. return; } - Configuration oldConfig = activity.getResources().getConfiguration(); + Configuration oldConfig = container.asContext().getResources().getConfiguration(); boolean isFoldUnfold = isTablet(oldConfig) != isTablet(newConfig); if (!isFoldUnfold && mOverviewComponentObserver.canHandleConfigChanges( - activity.getComponentName(), - activity.getResources().getConfiguration().diff(newConfig))) { + container.getComponentName(), + container.asContext().getResources().getConfiguration().diff(newConfig))) { // Since navBar gestural height are different between portrait and landscape, // can handle orientation changes and refresh navigation gestural region through // onOneHandedModeChanged() int newGesturalHeight = ResourceUtils.getNavbarSize( ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getApplicationContext().getResources()); - mDeviceState.onOneHandedModeChanged(newGesturalHeight); + mDeviceStateRepository.forEach(/* createIfAbsent= */ true, deviceState -> + deviceState.onOneHandedModeChanged(newGesturalHeight)); return; } - preloadOverview(false /* fromInit */); + ActivityPreloadUtil.preloadOverviewForTIS(this, false /* fromInit */); } private static boolean isTablet(Configuration config) { @@ -1520,7 +1376,6 @@ public class TouchInteractionService extends Service { if (LockedUserState.get(this).isUserUnlocked()) { PluginManagerWrapper.INSTANCE.get(getBaseContext()).dump(pw); } - mDeviceState.dump(pw); if (mOverviewComponentObserver != null) { mOverviewComponentObserver.dump(pw); } @@ -1533,41 +1388,176 @@ public class TouchInteractionService extends Service { pw.println("Input state:"); pw.println("\tmInputMonitorCompat=" + mInputMonitorCompat); pw.println("\tmInputEventReceiver=" + mInputEventReceiver); + if (mInputMonitorDisplayModel == null) { + pw.println("\tmInputMonitorDisplayModel=null"); + } else { + mInputMonitorDisplayModel.dump("\t", pw); + } DisplayController.INSTANCE.get(this).dump(pw); - pw.println("TouchState:"); - BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null - : mOverviewComponentObserver.getActivityInterface().getCreatedContainer(); - boolean resumed = mOverviewComponentObserver != null - && mOverviewComponentObserver.getActivityInterface().isResumed(); - pw.println("\tcreatedOverviewActivity=" + createdOverviewActivity); - pw.println("\tresumed=" + resumed); + mDisplayRepository.getDisplayIds().getValue().forEach(displayId -> { + pw.println(String.format(Locale.ENGLISH, "TouchState (displayId %d):", displayId)); + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId); + if (deviceState != null) { + deviceState.dump(pw); + } + BaseContainerInterface containerInterface = + mOverviewComponentObserver == null ? null + : mOverviewComponentObserver.getContainerInterface( + displayId); + RecentsViewContainer createdOverviewContainer = containerInterface == null ? null : + containerInterface.getCreatedContainer(); + boolean resumed = containerInterface != null && containerInterface.isResumed(); + pw.println("\tcreatedOverviewActivity=" + createdOverviewContainer); + pw.println("\tresumed=" + resumed); + if (createdOverviewContainer != null) { + createdOverviewContainer.getDeviceProfile().dump(this, "", pw); + } + TaskAnimationManager taskAnimationManager = mTaskAnimationManagerRepository.get( + displayId); + if (taskAnimationManager != null) { + taskAnimationManager.dump("\t", pw); + } + }); pw.println("\tmConsumer=" + mConsumer.getName()); ActiveGestureLog.INSTANCE.dump("", pw); RecentsModel.INSTANCE.get(this).dump("", pw); - if (mTaskAnimationManager != null) { - mTaskAnimationManager.dump("", pw); - } - if (createdOverviewActivity != null) { - createdOverviewActivity.getDeviceProfile().dump(this, "", pw); - } mTaskbarManager.dumpLogs("", pw); - pw.println("AssistStateManager:"); - AssistStateManager.INSTANCE.get(this).dump("\t", pw); + DesktopVisibilityController.INSTANCE.get(this).dumpLogs("", pw); + pw.println("ContextualSearchStateManager:"); + ContextualSearchStateManager.INSTANCE.get(this).dump("\t", pw); SystemUiProxy.INSTANCE.get(this).dump(pw); DeviceConfigWrapper.get().dump(" ", pw); + TopTaskTracker.INSTANCE.get(this).dump(pw); } - private AbsSwipeUpHandler createLauncherSwipeHandler( + private @Nullable AbsSwipeUpHandler createLauncherSwipeHandler( GestureState gestureState, long touchTimeMs) { - return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager, - gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(), - mInputConsumer); + int displayId = gestureState.getDisplayId(); + TaskAnimationManager taskAnimationManager = mTaskAnimationManagerRepository.get(displayId); + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId); + RotationTouchHelper rotationTouchHelper = mRotationTouchHelperRepository.get(displayId); + if (taskAnimationManager == null || deviceState == null || rotationTouchHelper == null) { + Log.d(TAG, "displayId " + displayId + " not valid"); + return null; + } + return new LauncherSwipeHandlerV2(this, taskAnimationManager, deviceState, + rotationTouchHelper, gestureState, touchTimeMs, + taskAnimationManager.isRecentsAnimationRunning(), + mInputConsumer, MSDLPlayerWrapper.INSTANCE.get(this)); } - private AbsSwipeUpHandler createFallbackSwipeHandler( + private @Nullable AbsSwipeUpHandler createFallbackSwipeHandler( GestureState gestureState, long touchTimeMs) { - return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager, - gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(), - mInputConsumer); + int displayId = gestureState.getDisplayId(); + TaskAnimationManager taskAnimationManager = mTaskAnimationManagerRepository.get(displayId); + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId); + RotationTouchHelper rotationTouchHelper = mRotationTouchHelperRepository.get(displayId); + if (taskAnimationManager == null || deviceState == null || rotationTouchHelper == null) { + Log.d(TAG, "displayId " + displayId + " not valid"); + return null; + } + return new FallbackSwipeHandler(this, taskAnimationManager, deviceState, + rotationTouchHelper, gestureState, touchTimeMs, + taskAnimationManager.isRecentsAnimationRunning(), + mInputConsumer, MSDLPlayerWrapper.INSTANCE.get(this)); + } + + private @Nullable AbsSwipeUpHandler createRecentsWindowSwipeHandler( + GestureState gestureState, long touchTimeMs) { + int displayId = gestureState.getDisplayId(); + TaskAnimationManager taskAnimationManager = mTaskAnimationManagerRepository.get(displayId); + RecentsAnimationDeviceState deviceState = mDeviceStateRepository.get(displayId); + RotationTouchHelper rotationTouchHelper = mRotationTouchHelperRepository.get(displayId); + RecentsWindowManager recentsWindowManager = mRecentsWindowManagerRepository.get(displayId); + if (taskAnimationManager == null || deviceState == null || rotationTouchHelper == null + || recentsWindowManager == null) { + Log.d(TAG, "displayId " + displayId + " not valid"); + return null; + } + return new RecentsWindowSwipeHandler(recentsWindowManager, + taskAnimationManager, deviceState, + rotationTouchHelper, recentsWindowManager, gestureState, touchTimeMs, + taskAnimationManager.isRecentsAnimationRunning(), + mInputConsumer, MSDLPlayerWrapper.INSTANCE.get(this)); + } + + private int focusedDisplayIdForOverviewOnConnectedDisplays() { + return enableOverviewOnConnectedDisplays() + ? SystemUiProxy.INSTANCE.get(this).getFocusState().getFocusedDisplayId() + : DEFAULT_DISPLAY; + } + + private int focusedDisplayIdForAltTabKqsOnConnectedDisplays() { + return enableAltTabKqsOnConnectedDisplays.isTrue() + ? SystemUiProxy.INSTANCE.get(this).getFocusState().getFocusedDisplayId() + : DEFAULT_DISPLAY; + } + + + private OverviewGestureHandler createOverviewGestureHandler() { + return new OverviewGestureHandler() { + @Override + public void showOverview(@NonNull OverviewType type) { + mTISBinder.onOverviewShown(/* triggeredFromAltTab= */ type == OverviewType.ALT_TAB); + } + + @Override + public void hideOverview(@NonNull OverviewType type) { + mTISBinder.onOverviewHidden( + /* triggeredFromAltTab= */ type == OverviewType.ALT_TAB, + /* triggeredFromHomeKey= */ type == OverviewType.HOME); + } + }; + } + + /** + * Helper class that keeps track of external displays and prepares input monitors for each. + */ + private class InputMonitorDisplayModel extends DisplayModel { + + private InputMonitorDisplayModel( + Context context, SystemDecorationChangeObserver systemDecorationChangeObserver) { + super(context, systemDecorationChangeObserver, mDisplaysWithDecorationsRepositoryCompat, + mCoroutineDispatcher); + initializeDisplays(); + } + + @NonNull + @Override + public InputMonitorResource createDisplayResource(@NonNull Display display) { + return new InputMonitorResource(display.getDisplayId()); + } + } + + private class InputMonitorResource extends DisplayModel.DisplayResource { + + private final int displayId; + + private final InputMonitorCompat inputMonitorCompat; + private final InputEventReceiver inputEventReceiver; + + private InputMonitorResource(int displayId) { + this.displayId = displayId; + inputMonitorCompat = new InputMonitorCompat("swipe-up", displayId); + inputEventReceiver = inputMonitorCompat.getInputReceiver( + Looper.getMainLooper(), + TouchInteractionService.this.mMainChoreographer, + TouchInteractionService.this::onInputEvent); + } + + @Override + public void cleanup() { + inputEventReceiver.dispose(); + inputMonitorCompat.dispose(); + } + + @Override + public void dump(String prefix , PrintWriter writer) { + writer.println(prefix + "InputMonitorResource:"); + + writer.println(prefix + "\tdisplayId=" + displayId); + writer.println(prefix + "\tinputMonitorCompat=" + inputMonitorCompat); + writer.println(prefix + "\tinputEventReceiver=" + inputEventReceiver); + } } } diff --git a/quickstep/src/com/android/quickstep/ViewUtils.java b/quickstep/src/com/android/quickstep/ViewUtils.java index e65511b77d..a88b3c20a5 100644 --- a/quickstep/src/com/android/quickstep/ViewUtils.java +++ b/quickstep/src/com/android/quickstep/ViewUtils.java @@ -23,6 +23,7 @@ import android.view.ViewRootImpl; import com.android.launcher3.Utilities; +import java.util.ArrayList; import java.util.function.BooleanSupplier; import app.lawnchair.compat.LawnchairQuickstepCompat; @@ -45,9 +46,6 @@ public class ViewUtils { */ public static boolean postFrameDrawn( View view, Runnable onFinishRunnable, BooleanSupplier canceled) { - if (!LawnchairQuickstepCompat.ATLEAST_U) { - return new FrameHandlerVR(view, onFinishRunnable, canceled).schedule(); - } return new FrameHandler(view, onFinishRunnable, canceled).schedule(); } @@ -135,50 +133,17 @@ public class ViewUtils { } } - private static class FrameHandlerVR implements HardwareRenderer.FrameDrawingCallback { - - final ViewRootImpl mViewRoot; - final Runnable mFinishCallback; - final BooleanSupplier mCancelled; - final Handler mHandler; - - int mDeferFrameCount = 1; - - FrameHandlerVR(View view, Runnable finishCallback, BooleanSupplier cancelled) { - mViewRoot = view.getViewRootImpl(); - mFinishCallback = finishCallback; - mCancelled = cancelled; - mHandler = new Handler(); - } - - @Override - public void onFrameDraw(long frame) { - Utilities.postAsyncCallback(mHandler, this::onFrame); - } - - private void onFrame() { - if (mCancelled.getAsBoolean()) { - return; - } - - if (mDeferFrameCount > 0) { - mDeferFrameCount--; - schedule(); - return; - } - - if (mFinishCallback != null) { - mFinishCallback.run(); - } - } - - private boolean schedule() { - if (mViewRoot != null && mViewRoot.getView() != null) { - mViewRoot.registerRtFrameCallback(this); - mViewRoot.getView().invalidate(); - return true; - } - return false; + /** + * Adds the view to the list of accessible children. + * + * @param view The view to add. + * @param outChildren The list of accessible children. + */ + public static void addAccessibleChildToList(View view, ArrayList outChildren) { + if (view.includeForAccessibility()) { + outChildren.add(view); + } else { + view.addChildrenForAccessibility(outChildren); } } } diff --git a/quickstep/src/com/android/quickstep/actioncorner/ActionCornerHandler.kt b/quickstep/src/com/android/quickstep/actioncorner/ActionCornerHandler.kt new file mode 100644 index 0000000000..5ddad7e493 --- /dev/null +++ b/quickstep/src/com/android/quickstep/actioncorner/ActionCornerHandler.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.actioncorner + +import android.app.ActivityOptions +import android.content.Context +import android.window.SplashScreen +import com.android.launcher3.Flags.enableReversibleHomeActionCorner +import com.android.launcher3.concurrent.annotations.LightweightBackground +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.util.ActivityOptionsWrapper +import com.android.launcher3.util.RunnableList +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.quickstep.OverviewCommandHelper +import com.android.quickstep.OverviewCommandHelper.CommandType +import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE_OVERVIEW_PREVIOUS +import com.android.quickstep.OverviewComponentObserver +import com.android.quickstep.RecentsModel +import com.android.quickstep.TopTaskTracker +import com.android.quickstep.util.AnimUtils +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SplitTask +import com.android.quickstep.views.RecentsViewContainer +import com.android.systemui.shared.system.ActivityManagerWrapper +import com.android.systemui.shared.system.actioncorner.ActionCornerConstants.Action +import com.android.systemui.shared.system.actioncorner.ActionCornerConstants.HOME +import com.android.systemui.shared.system.actioncorner.ActionCornerConstants.OVERVIEW +import com.android.wm.shell.shared.GroupedTaskInfo +import com.android.wm.shell.shared.split.SplitScreenConstants +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.concurrent.Executor +import java.util.function.Predicate + +/** + * Handles actions triggered from action corners that are mapped to specific functionalities. + * Launcher supports both overview and home actions. + */ +class ActionCornerHandler +@AssistedInject +constructor( + @ApplicationContext private val context: Context, + private val overviewComponentObserver: OverviewComponentObserver, + private val topTaskTracker: TopTaskTracker, + private val recentsModel: RecentsModel, + private val activityManagerWrapper: ActivityManagerWrapper, + @LightweightBackground private val executor: Executor, + @Assisted private val overviewCommandHelper: OverviewCommandHelper, +) { + @AssistedFactory + interface Factory { + fun create(overviewCommandHelper: OverviewCommandHelper): ActionCornerHandler + } + + private val displayToPreviousScreenMap = HashMap() + + fun handleAction(@Action action: Int, displayId: Int) { + when (action) { + // TODO(b/410798748): handle projected mode when launching overview + OVERVIEW -> overviewCommandHelper.addCommandsForAllDisplays(TOGGLE_OVERVIEW_PREVIOUS) + HOME -> + if (enableReversibleHomeActionCorner()) { + handleHomeAction(displayId) + } else { + overviewCommandHelper.addCommand(CommandType.HOME, displayId) + } + else -> {} + } + } + + private fun handleHomeAction(displayId: Int) { + val topTask = + topTaskTracker.getCachedTopTask(/* filterOnlyVisibleRecents= */ false, displayId) + val groupTask = + topTask.getPlaceholderGroupedTaskInfo(topTaskTracker.runningSplitTaskIds) ?: return + if (!groupTask.isBaseType(GroupedTaskInfo.TYPE_DESK)) { + if ( + topTask.isHomeTask && + getRecentsViewContainer(displayId)?.isRecentsViewVisible == false + ) { + goToPreviousScreen(displayId) + } else { + storeCurrentScreen(displayId, groupTask) + overviewCommandHelper.addCommand(CommandType.HOME, displayId) + } + } else { + // TODO(b/416664984): handle reversible home action for desktop mode + } + } + + private fun goToPreviousScreen(displayId: Int) { + val previousScreen = displayToPreviousScreenMap[displayId] + if (previousScreen is PreviousScreen.OverviewScreen) { + overviewCommandHelper.addCommand(CommandType.TOGGLE, displayId) + displayToPreviousScreenMap.remove(displayId) + } else { + // Get it from recentsModel so we have the latest split bound for split task. It also + // checks if the stored task is valid, it would not go to previous screen if the stored + // task is not the latest task. + recentsModel.getTasks(Predicate { it.displayId == displayId }) { + val latestTask = it.last() + val isSameAsStoredTask = + when (previousScreen) { + is PreviousScreen.SingleTaskScreen -> { + latestTask is SingleTask && + latestTask.task.key.id == previousScreen.taskId + } + + is PreviousScreen.SplitTaskScreen -> { + latestTask is SplitTask && + latestTask.topLeftTask.key.id == previousScreen.taskId1 && + latestTask.bottomRightTask.key.id == previousScreen.taskId2 + } + + else -> { + false + } + } + if (isSameAsStoredTask) { + launchGroupTask(latestTask, displayId) + } else { + // Remove the stored task as it is outdated. + displayToPreviousScreenMap.remove(displayId) + } + } + } + } + + private fun storeCurrentScreen(displayId: Int, groupTask: GroupedTaskInfo) { + if (getRecentsViewContainer(displayId)?.isRecentsViewVisible == true) { + displayToPreviousScreenMap[displayId] = PreviousScreen.OverviewScreen + } else { + if (groupTask.isBaseType(GroupedTaskInfo.TYPE_SPLIT)) { + displayToPreviousScreenMap[displayId] = + PreviousScreen.SplitTaskScreen( + groupTask.taskInfo1!!.taskId, + groupTask.taskInfo2!!.taskId, + ) + } else if (groupTask.isBaseType(GroupedTaskInfo.TYPE_FULLSCREEN)) { + displayToPreviousScreenMap[displayId] = + PreviousScreen.SingleTaskScreen(groupTask.taskInfo1!!.taskId) + } + } + } + + private fun launchGroupTask(task: GroupTask, displayId: Int) { + when (task) { + is SingleTask -> + executor.execute { + val activityOptions: ActivityOptions = + makeDefaultActivityOptions(displayId) ?: return@execute + activityManagerWrapper.startActivityFromRecents( + task.task.key.id, + activityOptions, + ) + } + is SplitTask -> { + val splitSelectStateController = + getRecentsViewContainer(displayId)?.splitSelectStateController + splitSelectStateController?.launchExistingSplitPair( + /* groupedTaskView= */ null, + task.topLeftTask.key.id, + task.bottomRightTask.key.id, + SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, + /* callback= */ { splitSelectStateController.resetState() }, + /* freezeTaskList= */ false, + task.splitBounds?.snapPosition ?: SplitScreenConstants.SNAP_TO_2_50_50, + ) + } + } + } + + private fun getRecentsViewContainer(displayId: Int): RecentsViewContainer? = + overviewComponentObserver.getContainerInterface(displayId)?.getCreatedContainer() + + private fun makeDefaultActivityOptions(displayId: Int): ActivityOptions? { + val callbacks = RunnableList() + val options = ActivityOptions.makeCustomAnimation(context, 0, 0) + options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED) + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS + ) + + val recentsViewContainer = getRecentsViewContainer(displayId) ?: return null + val endCallback = AnimUtils.completeRunnableListCallback(callbacks, recentsViewContainer) + options.setOnAnimationAbortListener(endCallback) + options.setOnAnimationFinishedListener(endCallback) + return ActivityOptionsWrapper(options, callbacks).options + } +} diff --git a/quickstep/src/com/android/quickstep/actioncorner/PreviousScreen.kt b/quickstep/src/com/android/quickstep/actioncorner/PreviousScreen.kt new file mode 100644 index 0000000000..8a72553a9c --- /dev/null +++ b/quickstep/src/com/android/quickstep/actioncorner/PreviousScreen.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.actioncorner + +/** Stores the screen before going home or overview for action corner. */ +sealed class PreviousScreen { + + data class SingleTaskScreen(val taskId: Int) : PreviousScreen() + + data class SplitTaskScreen(val taskId1: Int, val taskId2: Int) : PreviousScreen() + + data object OverviewScreen : PreviousScreen() +} diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseActivityComponent.kt b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseActivityComponent.kt new file mode 100644 index 0000000000..a34330c381 --- /dev/null +++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseActivityComponent.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.dagger + +import com.android.launcher3.dagger.BaseActivityContextComponent + +/** Activity Quickstep base component for Dagger injection. */ +interface QuickstepBaseActivityComponent : BaseActivityContextComponent {} diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java new file mode 100644 index 0000000000..4ffcef5075 --- /dev/null +++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java @@ -0,0 +1,107 @@ +/* + * 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.quickstep.dagger; + +import com.android.app.displaylib.DisplayRepository; +import com.android.app.displaylib.DisplaysWithDecorationsRepositoryCompat; +import com.android.app.displaylib.PerDisplayRepository; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherBaseAppComponent; +import com.android.launcher3.model.WellbeingModel; +import com.android.launcher3.statehandlers.DesktopVisibilityController; +import com.android.launcher3.taskbar.TaskbarModelCallbacksFactory; +import com.android.launcher3.taskbar.TaskbarViewCallbacksFactory; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContextFactory; +import com.android.quickstep.FallbackWindowInterface; +import com.android.quickstep.OverviewComponentObserver; +import com.android.quickstep.RecentsAnimationDeviceState; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.RotationTouchHelper; +import com.android.quickstep.SimpleOrientationTouchTransformer; +import com.android.quickstep.SystemDecorationChangeObserver; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TaskAnimationManager; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.actioncorner.ActionCornerHandler; +import com.android.quickstep.fallback.window.RecentsWindowManager; +import com.android.quickstep.inputconsumers.NavHandleLongPressHandler; +import com.android.quickstep.logging.SettingsChangeLogger; +import com.android.quickstep.util.AsyncClockEventDelegate; +import com.android.quickstep.util.ContextualSearchHapticManager; +import com.android.quickstep.util.ContextualSearchStateManager; +import com.android.quickstep.views.RecentsDismissUtils; + +/** + * Launcher Quickstep base component for Dagger injection. + * + * This class is not actually annotated as a Dagger component, since it is not used directly as one. + * Doing so generates unnecessary code bloat. + * + * See {@link LauncherAppComponent} for the one actually used. + */ +public interface QuickstepBaseAppComponent extends LauncherBaseAppComponent { + + WellbeingModel getWellbeingModel(); + + AsyncClockEventDelegate getAsyncClockEventDelegate(); + + SystemUiProxy getSystemUiProxy(); + + OverviewComponentObserver getOverviewComponentObserver(); + + DesktopVisibilityController getDesktopVisibilityController(); + + TopTaskTracker getTopTaskTracker(); + + ContextualSearchHapticManager getContextualSearchHapticManager(); + + ContextualSearchStateManager getContextualSearchStateManager(); + + PerDisplayRepository getRecentsAnimationDeviceStateRepository(); + + PerDisplayRepository getTaskAnimationManagerRepository(); + + PerDisplayRepository getRotationTouchHelperRepository(); + + PerDisplayRepository getRecentsWindowManagerRepository(); + + PerDisplayRepository getFallbackWindowInterfaceRepository(); + + RecentsModel getRecentsModel(); + + RecentsDismissUtils.Factory getRecentsDismissUtilsFactory(); + + SettingsChangeLogger getSettingsChangeLogger(); + + SimpleOrientationTouchTransformer getSimpleOrientationTouchTransformer(); + + SystemDecorationChangeObserver getSystemDecorationChangeObserver(); + + DisplayRepository getDisplayRepository(); + NavHandleLongPressHandler getNavHandleLongPressHandler(); + + /** Gets the factory to create a new ActionCornerHandlerFactory */ + ActionCornerHandler.Factory getActionCornerHandlerFactory(); + + DisplaysWithDecorationsRepositoryCompat getDisplaysWithDecorationsRepositoryCompat(); + + TaskbarModelCallbacksFactory getTaskbarModelCallbacksFactory(); + + TaskbarViewCallbacksFactory getTaskbarViewCallbacksFactory(); + + TaskbarOverlayContextFactory getTaskbarOverlayContextFactory(); +} diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java index 94764a58cc..ed03b36988 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java @@ -18,6 +18,9 @@ package com.android.quickstep.fallback; import static com.android.app.animation.Interpolators.FINAL_FRAME; import static com.android.app.animation.Interpolators.INSTANT; import static com.android.app.animation.Interpolators.LINEAR; +import static com.android.launcher3.Flags.enableDesktopExplodedView; +import static com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview; +import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE; import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X; @@ -26,6 +29,7 @@ import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE; import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW; import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; +import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS; import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; @@ -34,6 +38,7 @@ import static com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLA import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA; +import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS; import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL; import android.util.FloatProperty; @@ -47,9 +52,10 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; -import com.android.quickstep.RecentsActivity; +import com.android.quickstep.views.AddDesktopButton; import com.android.quickstep.views.ClearAllButton; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; /** * State controller for fallback recents activity @@ -57,12 +63,12 @@ import com.android.quickstep.views.RecentsView; public class FallbackRecentsStateController implements StateHandler { private final StateAnimationConfig mNoConfig = new StateAnimationConfig(); - private final RecentsActivity mActivity; + private final RecentsViewContainer mRecentsViewContainer; private final FallbackRecentsView mRecentsView; - public FallbackRecentsStateController(RecentsActivity activity) { - mActivity = activity; - mRecentsView = activity.getOverviewPanel(); + public FallbackRecentsStateController(RecentsViewContainer container) { + mRecentsViewContainer = container; + mRecentsView = container.getOverviewPanel(); } @Override @@ -81,7 +87,7 @@ public class FallbackRecentsStateController implements StateHandler mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL)); setter.addEndListener(success -> { - if (!success) { + if (!success && !toState.isRecentsViewVisible()) { mRecentsView.reset(); } }); @@ -93,13 +99,18 @@ public class FallbackRecentsStateController implements StateHandler, FloatProperty> taskViewsFloat = + Pair>, FloatProperty>> taskViewsFloat = mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset( TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION, - mActivity.getDeviceProfile()); + mRecentsViewContainer.getDeviceProfile()); setter.setFloat(mRecentsView, taskViewsFloat.first, isSplitSelectionState(state) ? mRecentsView.getSplitSelectTranslation() : 0, LINEAR); setter.setFloat(mRecentsView, taskViewsFloat.second, 0, LINEAR); diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java index f4a2738fef..7704684741 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java @@ -17,9 +17,9 @@ package com.android.quickstep.fallback; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview; import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS; import static com.android.quickstep.fallback.RecentsState.DEFAULT; -import static com.android.quickstep.fallback.RecentsState.HOME; import static com.android.quickstep.fallback.RecentsState.MODAL_TASK; import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT; @@ -33,30 +33,34 @@ import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.desktop.DesktopRecentsTransitionController; import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StateManager.StateListener; +import com.android.launcher3.statemanager.StatefulContainer; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; -import com.android.quickstep.FallbackActivityInterface; import com.android.quickstep.GestureState; -import com.android.quickstep.RecentsActivity; -import com.android.quickstep.RotationTouchHelper; +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SingleTask; import com.android.quickstep.util.SplitSelectStateController; -import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.GroupedTaskInfo; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class FallbackRecentsView extends RecentsView +public class FallbackRecentsView> extends RecentsView implements StateListener { private static final int TASK_DISMISS_DURATION = 150; @@ -69,7 +73,7 @@ public class FallbackRecentsView extends RecentsView getStateManager() { + public StateManager getStateManager() { return mContainer.getStateManager(); } @@ -102,12 +110,13 @@ public class FallbackRecentsView extends RecentsView 0 ? homeTask[0] : null; - onGestureAnimationStart(homeTask, rotationTouchHelper); + if (homeTaskInfo != null) { + mHomeTask = Task.from(homeTaskInfo.getTaskInfo1()); + } + onGestureAnimationStart(homeTaskInfo); } /** @@ -117,17 +126,19 @@ public class FallbackRecentsView extends RecentsView setCurrentTask(-1)); - AnimatorPlaybackController controller = pa.createPlaybackController(); + AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget, + RemoteTargetHandle[] remoteTargetHandles, boolean isHandlingAtomicEvent) { + super.onPrepareGestureEndAnimation(animatorSet, endTarget, remoteTargetHandles, + isHandlingAtomicEvent); + if (mHomeTask != null && endTarget == RECENTS) { + TaskView homeTaskView = getTaskViewByTaskId(mHomeTask.key.id); + if (homeTaskView != null) { + PendingAnimation pendingAnimation = new PendingAnimation(TASK_DISMISS_DURATION); + createTaskDismissAnimation(pendingAnimation, homeTaskView, true, false, + TASK_DISMISS_DURATION, false /* dismissingForSplitSelection*/, + null /* gridEndData */); + pendingAnimation.addEndListener(e -> setCurrentTask(-1)); + AnimatorPlaybackController controller = pendingAnimation.createPlaybackController(); controller.dispatchOnStart(); animatorSet.play(controller.getAnimationPlayer()); } @@ -161,26 +172,24 @@ public class FallbackRecentsView extends RecentsView 1) { + protected boolean shouldAvoidAddingStubTaskView(GroupedTaskInfo groupedTaskInfo) { + if (!groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_FULLSCREEN)) { // can't be in split screen w/ home task - return super.shouldAddStubTaskView(runningTasks); + return super.shouldAvoidAddingStubTaskView(groupedTaskInfo); } - Task runningTask = runningTasks[0]; - if (mHomeTask != null && runningTask != null - && mHomeTask.key.id == runningTask.key.id - && getTaskViewCount() == 0 && mLoadPlanEverApplied) { + if (mHomeTask != null && groupedTaskInfo.containsTask(mHomeTask.key.id) && !hasTaskViews() + && mLoadPlanEverApplied) { // Do not add a stub task if we are running over home with empty recents, so that we // show the empty recents message instead of showing a stub task and later removing it. - // Ignore empty task signal if applyLoadPlan has never run. - return false; + // Ignore empty task signal if [applyLoadPlan] has never run. + return true; } - return super.shouldAddStubTaskView(runningTasks); + return super.shouldAvoidAddingStubTaskView(groupedTaskInfo); } @Override - protected void applyLoadPlan(List taskGroups) { + protected void applyLoadPlan(List taskGroups, int taskListChangeId) { // When quick-switching on 3p-launcher, we add a "stub" tile corresponding to Launcher // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to // track the index of the next task appropriately, as if we are switching on any other app. @@ -200,11 +209,11 @@ public class FallbackRecentsView extends RecentsView newList = new ArrayList<>(taskGroups.size() + 1); newList.addAll(taskGroups); - newList.add(new GroupTask(mHomeTask, null, null)); + newList.add(new SingleTask(mHomeTask)); taskGroups = newList; } } - super.applyLoadPlan(taskGroups); + super.applyLoadPlan(taskGroups, taskListChangeId); } @Override @@ -229,17 +238,24 @@ public class FallbackRecentsView extends RecentsView - remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true)); + // disabling this so app icons aren't drawn on top of recent tasks. + if (isOverlayEnabled && !(mContainer instanceof RecentsWindowManager)) { + mBlurUtils.setDrawLiveTileBelowRecents(true); } } @@ -292,6 +312,7 @@ public class FallbackRecentsView extends RecentsView { +public class RecentsDragLayer> extends BaseDragLayer { + + private final TaskViewRecentsTouchContext mTaskViewRecentsTouchContext = + new TaskViewRecentsTouchContext() { + @Override + public boolean isRecentsInteractive() { + return mContainer.getRootView().hasWindowFocus() + || mContainer.getStateManager().getState().hasLiveTile(); + } + + @Override + public boolean isRecentsModal() { + return mContainer.isInState(MODAL_TASK); + } + }; public RecentsDragLayer(Context context, AttributeSet attrs) { super(context, attrs, 1 /* alphaChannelCount */); @@ -33,9 +56,19 @@ public class RecentsDragLayer extends BaseDragLayer { @Override public void recreateControllers() { - mControllers = new TouchController[] { - new RecentsTaskController(mActivity), - new FallbackNavBarTouchController(mActivity), - }; + super.recreateControllers(); + mControllers = enableExpressiveDismissTaskMotion() + ? new TouchController[]{ + new TaskViewLaunchTouchController<>(mContainer, + mTaskViewRecentsTouchContext), + new TaskViewDismissTouchController<>(mContainer, + mTaskViewRecentsTouchContext), + new FallbackNavBarTouchController(mContainer) + } + : new TouchController[]{ + new TaskViewTouchControllerDeprecated<>(mContainer, + mTaskViewRecentsTouchContext), + new FallbackNavBarTouchController(mContainer) + }; } } diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java index a47c6e4c70..29fbc53d27 100644 --- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java +++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java @@ -15,17 +15,22 @@ */ package com.android.quickstep.fallback; +import static com.android.launcher3.Flags.enableDesktopExplodedView; import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS; import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState; import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState; +import static com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview; import android.content.Context; import android.graphics.Color; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; import com.android.launcher3.statemanager.BaseState; import com.android.quickstep.RecentsActivity; import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.ScrimColors; import com.android.quickstep.views.RecentsViewContainer; import app.lawnchair.theme.color.tokens.ColorTokens; @@ -44,22 +49,40 @@ public class RecentsState implements BaseState { private static final int FLAG_LIVE_TILE = BaseState.getFlag(6); private static final int FLAG_RECENTS_VIEW_VISIBLE = BaseState.getFlag(7); private static final int FLAG_TASK_THUMBNAIL_SPLASH = BaseState.getFlag(8); + private static final int FLAG_ADD_DESK_BUTTON = BaseState.getFlag(9); + private static final int FLAG_SHOW_EXPLODED_DESKTOP_VIEW = BaseState.getFlag(10); - public static final RecentsState DEFAULT = new RecentsState(0, + public static final int DEFAULT_STATE_ORDINAL = 0; + public static final int MODAL_TASK_ORDINAL = 1; + public static final int BACKGROUND_APP_ORDINAL = 2; + public static final int HOME_STATE_ORDINAL = 3; + public static final int BG_LAUNCHER_ORDINAL = 4; + public static final int OVERVIEW_SPLIT_SELECT_ORDINAL = 5; + + private static final RecentsState[] sAllStates = new RecentsState[6]; + + public static final RecentsState DEFAULT = new RecentsState(DEFAULT_STATE_ORDINAL, FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID - | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE); - public static final RecentsState MODAL_TASK = new ModalState(1, - FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_MODAL - | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE); - public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2, + | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE + | FLAG_ADD_DESK_BUTTON | FLAG_SHOW_EXPLODED_DESKTOP_VIEW); + public static final RecentsState MODAL_TASK = new ModalState(MODAL_TASK_ORDINAL, + FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_ACTIONS | FLAG_MODAL + | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE + | FLAG_SHOW_EXPLODED_DESKTOP_VIEW); + public static final RecentsState BACKGROUND_APP = new BackgroundAppState(BACKGROUND_APP_ORDINAL, FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN - | FLAG_RECENTS_VIEW_VISIBLE - | FLAG_TASK_THUMBNAIL_SPLASH); - public static final RecentsState HOME = new RecentsState(3, 0); - public static final RecentsState BG_LAUNCHER = new LauncherState(4, 0); - public static final RecentsState OVERVIEW_SPLIT_SELECT = new RecentsState(5, + | FLAG_RECENTS_VIEW_VISIBLE | FLAG_TASK_THUMBNAIL_SPLASH); + public static final RecentsState HOME = new RecentsState(HOME_STATE_ORDINAL, 0); + public static final RecentsState BG_LAUNCHER = new LauncherState(BG_LAUNCHER_ORDINAL, 0); + public static final RecentsState OVERVIEW_SPLIT_SELECT = new RecentsState( + OVERVIEW_SPLIT_SELECT_ORDINAL, FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_RECENTS_VIEW_VISIBLE | FLAG_CLOSE_POPUPS - | FLAG_DISABLE_RESTORE); + | FLAG_DISABLE_RESTORE | FLAG_SHOW_EXPLODED_DESKTOP_VIEW); + + /** Returns the corresponding RecentsState from ordinal provided */ + public static RecentsState stateFromOrdinal(int ordinal) { + return sAllStates[ordinal]; + } public final int ordinal; private final int mFlags; @@ -70,11 +93,21 @@ public class RecentsState implements BaseState { public RecentsState(int id, int flags) { this.ordinal = id; this.mFlags = flags; + sAllStates[id] = this; } + @Override public String toString() { - return "Ordinal-" + ordinal; + return switch (ordinal) { + case DEFAULT_STATE_ORDINAL -> "RECENTS_DEFAULT"; + case MODAL_TASK_ORDINAL -> "RECENTS_MODAL_TASK"; + case BACKGROUND_APP_ORDINAL -> "RECENTS_BACKGROUND_APP"; + case HOME_STATE_ORDINAL -> "RECENTS_HOME"; + case BG_LAUNCHER_ORDINAL -> "RECENTS_BG_LAUNCHER"; + case OVERVIEW_SPLIT_SELECT_ORDINAL -> "RECENTS_SPLIT_SELECT"; + default -> "RECENTS Unknown Ordinal-" + ordinal; + }; } @Override @@ -83,7 +116,7 @@ public class RecentsState implements BaseState { } @Override - public int getTransitionDuration(Context context, boolean isToState) { + public int getTransitionDuration(ActivityContext context, boolean isToState) { return 250; } @@ -93,8 +126,7 @@ public class RecentsState implements BaseState { } /** - * For this state, how modal should over view been shown. 0 modalness means all - * tasks drawn, + * For this state, how modal should over view been shown. 0 modalness means all tasks drawn, * 1 modalness means the current task is show on its own. */ public float getOverviewModalness() { @@ -112,6 +144,13 @@ public class RecentsState implements BaseState { return hasFlag(FLAG_CLEAR_ALL_BUTTON); } + /** + * For this state, whether add desk button should be shown. + */ + public boolean hasAddDeskButton() { + return hasFlag(FLAG_ADD_DESK_BUTTON); + } + /** * For this state, whether overview actions should be shown. */ @@ -129,9 +168,12 @@ public class RecentsState implements BaseState { /** * For this state, what color scrim should be drawn behind overview. */ - public int getScrimColor(RecentsActivity activity) { - return hasFlag(FLAG_SCRIM) ? ColorTokens.OverviewScrim.resolveColor(activity) - : Color.TRANSPARENT; + public ScrimColors getScrimColor(Context context) { + return new ScrimColors( + /* backgroundColor */ hasFlag(FLAG_SCRIM) + ? Themes.getAttrColor(context, R.attr.overviewScrimColor) + : Color.TRANSPARENT, + /* foregroundColor */ Color.TRANSPARENT); } public float[] getOverviewScaleAndOffset(RecentsViewContainer container) { @@ -142,7 +184,7 @@ public class RecentsState implements BaseState { * For this state, whether tasks should layout as a grid rather than a list. */ public boolean displayOverviewTasksAsGrid(DeviceProfile deviceProfile) { - return hasFlag(FLAG_SHOW_AS_GRID) && deviceProfile.isTablet; + return hasFlag(FLAG_SHOW_AS_GRID) && deviceProfile.getDeviceProperties().isTablet(); } @Override @@ -150,6 +192,11 @@ public class RecentsState implements BaseState { return hasFlag(FLAG_TASK_THUMBNAIL_SPLASH); } + @Override + public boolean showExplodedDesktopView() { + return hasFlag(FLAG_SHOW_EXPLODED_DESKTOP_VIEW) && enableDesktopExplodedView(); + } + /** * True if the state has overview panel visible. */ @@ -165,6 +212,9 @@ public class RecentsState implements BaseState { @Override public float[] getOverviewScaleAndOffset(RecentsViewContainer container) { + if (enableGridOnlyOverview()) { + return super.getOverviewScaleAndOffset(container); + } return getOverviewScaleAndOffsetForModalState(container.getOverviewPanel()); } } diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsStateUtils.kt b/quickstep/src/com/android/quickstep/fallback/RecentsStateUtils.kt new file mode 100644 index 0000000000..259ff17bbd --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/RecentsStateUtils.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.fallback + +import com.android.launcher3.LauncherState + +fun RecentsState.toLauncherState(): LauncherState { + return when (ordinal) { + RecentsState.DEFAULT_STATE_ORDINAL -> LauncherState.OVERVIEW + RecentsState.MODAL_TASK_ORDINAL -> LauncherState.OVERVIEW_MODAL_TASK + RecentsState.BACKGROUND_APP_ORDINAL -> LauncherState.BACKGROUND_APP + RecentsState.HOME_STATE_ORDINAL -> LauncherState.NORMAL + RecentsState.BG_LAUNCHER_ORDINAL -> LauncherState.NORMAL + RecentsState.OVERVIEW_SPLIT_SELECT_ORDINAL -> LauncherState.OVERVIEW_SPLIT_SELECT + else -> LauncherState.NORMAL + } +} + +fun LauncherState.hasEquivalentRecentsState(): Boolean { + return when (this) { + LauncherState.OVERVIEW, + LauncherState.OVERVIEW_MODAL_TASK, + LauncherState.BACKGROUND_APP, + LauncherState.NORMAL, + LauncherState.OVERVIEW_SPLIT_SELECT -> true + else -> false + } +} + +fun RecentsState.toLauncherStateOrdinal(): Int = toLauncherState().ordinal diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java deleted file mode 100644 index 2cb398cfff..0000000000 --- a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.fallback; - -import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController; -import com.android.quickstep.RecentsActivity; - -public class RecentsTaskController extends TaskViewTouchController { - - public RecentsTaskController(RecentsActivity activity) { - super(activity); - } - - @Override - protected boolean isRecentsInteractive() { - return mContainer.hasWindowFocus() || mContainer.getStateManager().getState().hasLiveTile(); - } - - @Override - protected boolean isRecentsModal() { - return false; - } -} diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt new file mode 100644 index 0000000000..d70d7eb094 --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt @@ -0,0 +1,100 @@ +/* + * 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.quickstep.fallback.window + +import android.content.Context +import android.graphics.PixelFormat +import android.view.Display +import android.view.ViewGroup +import android.view.WindowManager +import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS +import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS +import com.android.launcher3.DeviceProfile +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.util.BaseContext +import com.android.launcher3.util.Themes + +/** + * Window context for the Overview overlays. + * + *

+ * Overlays have their own window and need a window context. + */ +abstract class RecentsWindowContext(windowContext: Context, wallpaperColorHints: Int) : + BaseContext( + base = windowContext, + themeResId = Themes.getActivityThemeRes(windowContext, wallpaperColorHints), + destroyOnDetach = false, + ) { + + private var deviceProfile: DeviceProfile? = null + + private val windowTitle: String = "RecentsWindow" + + protected var windowLayoutParams: WindowManager.LayoutParams? = + createDefaultWindowLayoutParams( + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + windowTitle, + ) + + fun initDeviceProfile() { + deviceProfile = + if (displayId == Display.DEFAULT_DISPLAY) + InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this) + else InvariantDeviceProfile.INSTANCE[this].createDeviceProfileForSecondaryDisplay(this) + } + + override fun getDeviceProfile(): DeviceProfile { + if (deviceProfile == null) { + initDeviceProfile() + } + return deviceProfile!! + } + + /** + * Creates LayoutParams for adding a view directly to WindowManager as a new window. + * + * @param type The window type to pass to the created WindowManager.LayoutParams. + * @param title The window title to pass to the created WindowManager.LayoutParams. + */ + private fun createDefaultWindowLayoutParams( + type: Int, + title: String, + ): WindowManager.LayoutParams { + var windowFlags = + (WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or + WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) + + val windowLayoutParams = + WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + type, + windowFlags, + PixelFormat.TRANSLUCENT, + ) + + windowLayoutParams.title = title + windowLayoutParams.fitInsetsTypes = 0 + windowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + windowLayoutParams.isSystemApplicationOverlay = true + windowLayoutParams.privateFlags = PRIVATE_FLAG_CONSUME_IME_INSETS + + return windowLayoutParams + } +} diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt new file mode 100644 index 0000000000..a3fd23ea79 --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.fallback.window + +//import android.window.DesktopExperienceFlags.DesktopExperienceFlag +import com.android.launcher3.Flags + +// Lawnchair-TODO-Flags: @JvmField +// val enableLauncherOverviewInWindow: DesktopExperienceFlag = +// DesktopExperienceFlag( +// Flags::enableLauncherOverviewInWindow, +// false, +// Flags.FLAG_ENABLE_LAUNCHER_OVERVIEW_IN_WINDOW, +// ) +// Impl: var.isTrue() +object RecentsWindowFlags { + @JvmField + val enableLauncherOverviewInWindow = false + + @JvmField + val enableFallbackOverviewInWindow = false + + @JvmField + val enableOverviewOnConnectedDisplays = false + + @JvmStatic + val enableOverviewInWindow + get() = false + + @JvmStatic + val enableDesktopMenuOnSecondaryDisplay: Boolean + get() = false + + @JvmStatic fun enableOverviewOnConnectedDisplays() = enableOverviewOnConnectedDisplays +} diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt new file mode 100644 index 0000000000..2d7035f191 --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt @@ -0,0 +1,585 @@ +/* + * 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.quickstep.fallback.window + +import android.animation.AnimatorSet +import android.app.ActivityOptions +import android.content.ComponentName +import android.content.Context +import android.content.LocusId +import android.content.res.Configuration +import android.os.Bundle +import android.view.Display.DEFAULT_DISPLAY +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.RemoteAnimationAdapter +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.View +import android.view.WindowManager +import android.window.RemoteTransition +import com.android.app.displaylib.PerDisplayInstanceProviderWithTeardown +import com.android.app.displaylib.PerDisplayRepository +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.BaseActivity +import com.android.launcher3.LauncherAnimationRunner +import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory +import com.android.launcher3.R +import com.android.launcher3.compat.AccessibilityManagerCompat +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.dagger.WindowContext +import com.android.launcher3.desktop.DesktopRecentsTransitionController +import com.android.launcher3.statemanager.StateManager +import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory +import com.android.launcher3.statemanager.StatefulContainer +import com.android.launcher3.taskbar.TaskbarUIController +import com.android.launcher3.testing.TestLogging +import com.android.launcher3.testing.shared.TestProtocol.SEQUENCE_MAIN +import com.android.launcher3.util.ContextTracker +import com.android.launcher3.util.DaggerSingletonObject +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.Executors +import com.android.launcher3.util.RunnableList +import com.android.launcher3.util.ScreenOnTracker +import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener +import com.android.launcher3.util.SystemUiController +import com.android.launcher3.util.WallpaperColorHints +import com.android.launcher3.views.BaseDragLayer +import com.android.launcher3.views.ScrimView +import com.android.quickstep.BaseContainerInterface +import com.android.quickstep.FallbackWindowInterface +import com.android.quickstep.HomeVisibilityState +import com.android.quickstep.OverviewComponentObserver +import com.android.quickstep.RecentsAnimationCallbacks +import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener +import com.android.quickstep.RecentsAnimationController +import com.android.quickstep.RecentsModel +import com.android.quickstep.RemoteAnimationTargets +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.dagger.QuickstepBaseAppComponent +import com.android.quickstep.fallback.FallbackRecentsStateController +import com.android.quickstep.fallback.FallbackRecentsView +import com.android.quickstep.fallback.RecentsDragLayer +import com.android.quickstep.fallback.RecentsState +import com.android.quickstep.fallback.RecentsState.BACKGROUND_APP +import com.android.quickstep.fallback.RecentsState.BG_LAUNCHER +import com.android.quickstep.fallback.RecentsState.DEFAULT +import com.android.quickstep.fallback.RecentsState.HOME +import com.android.quickstep.fallback.RecentsState.MODAL_TASK +import com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT +import com.android.quickstep.fallback.toLauncherStateOrdinal +import com.android.quickstep.util.RecentsAtomicAnimationFactory +import com.android.quickstep.util.RecentsWindowProtoLogProxy +import com.android.quickstep.util.SplitSelectStateController +import com.android.quickstep.util.TISBindHelper +import com.android.quickstep.views.OverviewActionsView +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.systemui.shared.recents.model.ThumbnailData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import javax.inject.Inject + +/** + * Class that will manage RecentsView lifecycle within a window and interface correctly where + * needed. This allows us to run RecentsView in a window where needed. + * + * todo: b/365776320,b/365777482 + * + * To add new protologs, see [RecentsWindowProtoLogProxy]. To enable logging to logcat, see + * [QuickstepProtoLogGroup.Constants.DEBUG_RECENTS_WINDOW] + */ +class RecentsWindowManager +@AssistedInject +constructor( + @Assisted windowContext: Context, + @Assisted private val fallbackWindowInterface: FallbackWindowInterface, + wallpaperColorHints: WallpaperColorHints, + private val systemUiProxy: SystemUiProxy, + private val recentsModel: RecentsModel, + private val screenOnTracker: ScreenOnTracker, +) : + RecentsWindowContext(windowContext, wallpaperColorHints.hints), + RecentsViewContainer, + StatefulContainer { + + companion object { + private const val HOME_APPEAR_DURATION: Long = 250 + private const val TAG = "RecentsWindowManager" + + @JvmField + val REPOSITORY_INSTANCE = + DaggerSingletonObject>( + QuickstepBaseAppComponent::getRecentsWindowManagerRepository + ) + + class RecentsWindowTracker : ContextTracker() { + override fun isHomeStarted(context: RecentsWindowManager?): Boolean { + // if we need to change this block to use context in some way, we will need to + // refactor RecentsWindowTracker to be an instance (instead of a singleton) managed + // by PerDisplayRepository. Otherwise bad things will occur. + return true + } + } + + @JvmStatic val recentsWindowTracker = RecentsWindowTracker() + } + + protected var recentsView: FallbackRecentsView? = null + private val windowManager: WindowManager = getSystemService(WindowManager::class.java)!! + private var layoutInflater: LayoutInflater = LayoutInflater.from(this).cloneInContext(this) + private var stateManager: StateManager = + StateManager(this, RecentsState.BG_LAUNCHER) + private var systemUiController: SystemUiController? = null + + private var dragLayer: RecentsDragLayer? = null + private var windowView: View? = null + private var actionsView: OverviewActionsView<*>? = null + private var scrimView: ScrimView? = null + + private var callbacks: RecentsAnimationCallbacks? = null + + private var taskbarUIController: TaskbarUIController? = null + private val tisBindHelper: TISBindHelper = TISBindHelper(this) {} + private val splitSelectStateController: SplitSelectStateController = + SplitSelectStateController( + /* container= */ this, + stateManager, + /* depthController= */ null, + statsLogManager, + systemUiProxy, + recentsModel, + /* activityBackCallback= */ null, + ) + + // Callback array that corresponds to events defined in @ActivityEvent + private val eventCallbacks = + listOf(RunnableList(), RunnableList(), RunnableList(), RunnableList()) + + private val animationToHomeFactory = + RemoteAnimationFactory { + _: Int, + appTargets: Array?, + wallpaperTargets: Array?, + nonAppTargets: Array?, + result: LauncherAnimationRunner.AnimationResult? -> + val controller = + getStateManager().createAnimationToNewWorkspace(BG_LAUNCHER, HOME_APPEAR_DURATION) + controller.dispatchOnStart() + val targets = + RemoteAnimationTargets( + appTargets, + wallpaperTargets, + nonAppTargets, + RemoteAnimationTarget.MODE_OPENING, + ) + for (app in targets.apps) { + SurfaceControl.Transaction().setAlpha(app.leash, 1f).apply() + } + val anim = AnimatorSet() + anim.play(controller.animationPlayer) + anim.setDuration(HOME_APPEAR_DURATION) + result!!.setAnimation( + anim, + this@RecentsWindowManager, + { + getStateManager().goToState(BG_LAUNCHER, true) + cleanupRecentsWindow() + }, + true, /* skipFirstFrame */ + ) + } + + private val onBackInvokedCallback: () -> Unit = { + // If we are in live tile mode, launch the live task, otherwise return home + recentsView?.runningTaskView?.launchWithAnimation() ?: startHome() + TestLogging.recordEvent(SEQUENCE_MAIN, "onBackInvoked") + } + + private val homeVisibilityState = SystemUiProxy.INSTANCE.get(this).homeVisibilityState + private val homeVisibilityListener = + object : HomeVisibilityState.VisibilityChangeListener { + override fun onHomeVisibilityChanged(isVisible: Boolean) { + if (isShowing() && !isVisible && isInState(DEFAULT)) { + // handling state where we end recents animation by swiping livetile away + // TODO: animate this switch. + cleanupRecentsWindow() + } + } + } + + private val recentsAnimationListener = + object : RecentsAnimationListener { + override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap) { + recentAnimationStopped() + } + + override fun onRecentsAnimationFinished(controller: RecentsAnimationController) { + recentAnimationStopped() + } + } + + private val screenChangedListener = ScreenOnListener { isOn -> + if (!isOn) { + cleanupRecentsWindow() + } + } + + init { + fallbackWindowInterface.setRecentsWindowManager(this) + homeVisibilityState.addListener(homeVisibilityListener) + } + + override fun handleConfigurationChanged(configuration: Configuration?) { + initDeviceProfile() + AbstractFloatingView.closeOpenViews( + this, + true, + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv(), + ) + dispatchDeviceProfileChanged() + } + + override fun destroy() { + super.destroy() + fallbackWindowInterface.setRecentsWindowManager(null) + tisBindHelper.onDestroy() + Executors.MAIN_EXECUTOR.execute { + onViewDestroyed() + cleanupRecentsWindow() + callbacks?.removeListener(recentsAnimationListener) + homeVisibilityState.removeListener(homeVisibilityListener) + recentsWindowTracker.onContextDestroyed(this) + recentsView?.destroy() + } + } + + fun startRecentsWindow(callbacks: RecentsAnimationCallbacks? = null) { + RecentsWindowProtoLogProxy.logStartRecentsWindow(isShowing(), windowView == null) + if (isShowing()) { + return + } + theme.applyStyle(overviewBlurStyleResId, true) + if (windowView == null) { + windowView = layoutInflater.inflate(R.layout.fallback_recents_activity, null) + } + + windowManager.addView(windowView, windowLayoutParams) + + windowView?.let { + actionsView = it.findViewById(R.id.overview_actions_view) + recentsView = + it.findViewById?>(R.id.overview_panel) + ?.apply { + init( + actionsView, + splitSelectStateController, + DesktopRecentsTransitionController( + stateManager, + systemUiProxy, + iApplicationThread, + /* depthController= */ null, + ), + ) + } + actionsView?.apply { + updateDimension(getDeviceProfile(), recentsView?.lastComputedTaskSize) + updateVerticalMargin(DisplayController.getNavigationMode(this@RecentsWindowManager)) + } + scrimView = it.findViewById(R.id.scrim_view) + dragLayer = it.findViewById(R.id.drag_layer) + + it.findOnBackInvokedDispatcher() + ?.registerSystemOnBackInvokedCallback(onBackInvokedCallback) + + it.systemUiVisibility = + (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) + } + + systemUiController = SystemUiController(windowView) + recentsWindowTracker.handleCreate(this) + + this.callbacks = callbacks + callbacks?.addListener(recentsAnimationListener) + screenOnTracker.addListener(screenChangedListener) + onViewCreated() + } + + override fun startHome() { + startHome(/* finishRecentsAnimation= */ true) + } + + fun startHome(finishRecentsAnimation: Boolean) { + val recentsView: RecentsView<*, *> = getOverviewPanel() + + // Don't go to home on connected displays + if (displayId != DEFAULT_DISPLAY) { + recentsView.runningTaskView?.launchWithAnimation() + return + } + + if (!finishRecentsAnimation) { + recentsView.switchToScreenshot /* onFinishRunnable= */ {} + startHomeInternal() + return + } + recentsView.switchToScreenshot { + recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() } + } + } + + private fun startHomeInternal() { + val displayId = displayId + val runner = LauncherAnimationRunner(mainThreadHandler, animationToHomeFactory, true) + val options = + ActivityOptions.makeRemoteAnimation( + RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0), + RemoteTransition( + runner.toRemoteTransition(), + iApplicationThread, + "StartHomeFromRecents", + ), + ) + options.launchDisplayId = displayId + OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG, displayId) + stateManager.moveToRestState() + } + + fun cleanupRecentsWindow() { + RecentsWindowProtoLogProxy.logCleanup(isShowing()) + if (isShowing()) { + AbstractFloatingView.closeAllOpenViews(this, /* animate= */ false) + windowManager.removeViewImmediate(windowView) + } + stateManager.moveToRestState() + callbacks?.removeListener(recentsAnimationListener) + callbacks = null + screenOnTracker.removeListener(screenChangedListener) + } + + private fun isShowing(): Boolean { + return windowView?.parent != null + } + + private fun recentAnimationStopped() { + if (isInState(BACKGROUND_APP)) { + cleanupRecentsWindow() + } + } + + override fun getComponentName(): ComponentName { + return ComponentName(this, RecentsWindowManager::class.java) + } + + override fun canStartHomeSafely(): Boolean { + val overviewCommandHelper = tisBindHelper.overviewCommandHelper + return overviewCommandHelper == null || + overviewCommandHelper.canStartHomeSafely() || + displayId != DEFAULT_DISPLAY + } + + override fun setTaskbarUIController(taskbarUIController: TaskbarUIController?) { + this.taskbarUIController = taskbarUIController + } + + override fun getTaskbarUIController(): TaskbarUIController? { + return taskbarUIController + } + + override fun collectStateHandlers(out: MutableList>?) { + out!!.add(FallbackRecentsStateController(this)) + } + + override fun getStateManager(): StateManager { + return this.stateManager + } + + override fun shouldAnimateStateChange(): Boolean { + return false + } + + override fun isInState(state: RecentsState?): Boolean { + return stateManager.state == state + } + + override fun onStateSetStart(state: RecentsState) { + super.onStateSetStart(state) + RecentsWindowProtoLogProxy.logOnStateSetStart(state.toString()) + } + + override fun onStateSetEnd(state: RecentsState) { + super.onStateSetEnd(state) + RecentsWindowProtoLogProxy.logOnStateSetEnd(state.toString()) + if (!state.isRecentsViewVisible) { + cleanupRecentsWindow() + } + AccessibilityManagerCompat.sendStateEventToTest(baseContext, state.toLauncherStateOrdinal()) + } + + override fun onRepeatStateSetAborted(state: RecentsState) { + super.onRepeatStateSetAborted(state) + RecentsWindowProtoLogProxy.logOnRepeatStateSetAborted(state.toString()) + if (!state.isRecentsViewVisible) { + cleanupRecentsWindow() + } + } + + override fun getSystemUiController(): SystemUiController? { + if (systemUiController == null) { + systemUiController = SystemUiController(rootView) + } + return systemUiController + } + + override fun getScrimView(): ScrimView? { + return scrimView + } + + override fun ?> getContainerInterface(): T { + return fallbackWindowInterface as T + } + + override fun getOverviewPanel(): T { + return recentsView as T + } + + override fun getSplitSelectStateController(): SplitSelectStateController { + return splitSelectStateController + } + + override fun getRootView(): View? { + return windowView + } + + override fun getDragLayer(): BaseDragLayer { + return dragLayer!! + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { + return windowView?.dispatchGenericMotionEvent(ev) ?: false + } + + override fun dispatchKeyEvent(ev: KeyEvent?): Boolean { + return windowView?.dispatchKeyEvent(ev) ?: false + } + + override fun onRootViewDispatchKeyEvent(event: KeyEvent?): Boolean { + TestLogging.recordKeyEvent(SEQUENCE_MAIN, "Key event", event) + return if ( + event?.action != KeyEvent.ACTION_DOWN || event.keyCode != KeyEvent.KEYCODE_ESCAPE + ) { + super.onRootViewDispatchKeyEvent(event) + } else if (isInState(OVERVIEW_SPLIT_SELECT) || isInState(MODAL_TASK)) { + stateManager.goToState(DEFAULT, true) + true + } else if (isInState(DEFAULT)) { + stateManager.goToState(HOME, true) + true + } else { + super.onRootViewDispatchKeyEvent(event) + } + } + + override fun getActionsView(): OverviewActionsView<*>? { + return actionsView + } + + override fun addForceInvisibleFlag(flag: Int) {} + + override fun clearForceInvisibleFlag(flag: Int) {} + + override fun setLocusContext(id: LocusId?, bundle: Bundle?) { + // no op + } + + override fun isStarted(): Boolean { + return isShowing() && stateManager.state.isRecentsViewVisible + } + + /** Adds a callback for the provided activity event */ + override fun addEventCallback(@BaseActivity.ActivityEvent event: Int, callback: Runnable?) { + eventCallbacks[event].add(callback) + } + + /** Removes a previously added callback */ + override fun removeEventCallback(@BaseActivity.ActivityEvent event: Int, callback: Runnable?) { + eventCallbacks[event].remove(callback) + } + + override fun runOnBindToTouchInteractionService(r: Runnable?) { + tisBindHelper.runOnBindToTouchInteractionService(r) + } + + override fun addMultiWindowModeChangedListener( + listener: BaseActivity.MultiWindowModeChangedListener? + ) { + // TODO(b/368408838) + } + + override fun removeMultiWindowModeChangedListener( + listener: BaseActivity.MultiWindowModeChangedListener? + ) {} + + override fun returnToHomescreen() { + startHome() + } + + override fun isRecentsViewVisible(): Boolean { + return isShowing() || getStateManager().state!!.isRecentsViewVisible + } + + override fun createAtomicAnimationFactory(): AtomicAnimationFactory? { + return RecentsAtomicAnimationFactory(this) + } + + override fun getOverviewBlurStyleResId(): Int { + return R.style.OverviewBlurFallbackStyle + } + + @AssistedFactory + interface Factory { + /** Creates a new instance of [RecentsWindowManager] for a given [context]. */ + fun create( + @WindowContext context: Context, + fallbackWindowInterface: FallbackWindowInterface, + ): RecentsWindowManager + } +} + +@LauncherAppSingleton +class RecentsWindowManagerInstanceProvider +@Inject +constructor( + private val factory: RecentsWindowManager.Factory, + @WindowContext private val windowContextRepository: PerDisplayRepository, + private val fallbackWindowInterfaceRepository: PerDisplayRepository, +) : PerDisplayInstanceProviderWithTeardown { + override fun createInstance(displayId: Int) = + windowContextRepository[displayId]?.let { windowContext -> + fallbackWindowInterfaceRepository[displayId]?.let { fallbackWindowInterface -> + factory.create(windowContext, fallbackWindowInterface) + } + } + + override fun destroyInstance(instance: RecentsWindowManager) { + instance.destroy() + } +} diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java new file mode 100644 index 0000000000..2fd744f648 --- /dev/null +++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java @@ -0,0 +1,507 @@ +/* + * 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.quickstep.fallback.window; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.content.Intent.EXTRA_COMPONENT_NAME; +import static android.content.Intent.EXTRA_USER; + +import static com.android.app.animation.Interpolators.ACCELERATE; +import static com.android.launcher3.GestureNavContract.EXTRA_ENABLE_GESTURE_CONTRACT; +import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT; +import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION; +import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE; +import static com.android.launcher3.GestureNavContract.EXTRA_ON_FINISH_CALLBACK; +import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK; +import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; +import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely; + +import android.animation.Animator; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; +import android.view.RemoteAnimationTarget; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.animation.Interpolator; +import android.window.TransitionInfo; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.PendingAnimation; +import com.android.launcher3.anim.SpringAnimationBuilder; +import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.MSDLPlayerWrapper; +import com.android.quickstep.AbsSwipeUpHandler; +import com.android.quickstep.GestureState; +import com.android.quickstep.RecentsAnimationController; +import com.android.quickstep.RecentsAnimationDeviceState; +import com.android.quickstep.RecentsAnimationTargets; +import com.android.quickstep.RotationTouchHelper; +import com.android.quickstep.TaskAnimationManager; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsState; +import com.android.quickstep.util.RectFSpringAnim; +import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; +import com.android.quickstep.util.TransformParams; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.Task.TaskKey; +import com.android.systemui.shared.system.InputConsumerController; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * Handles the navigation gestures when a 3rd party launcher is the default home activity. + * + * Bugs: b/365775417 + */ +public class RecentsWindowSwipeHandler extends AbsSwipeUpHandler, RecentsState> { + + private static final String TAG = "RecentsWindowSwipeHandler"; + + /** + * Message used for receiving gesture nav contract information. We use a static messenger to + * avoid leaking too make binders in case the receiving launcher does not handle the contract + * properly. + */ + private static StaticMessageReceiver sMessageReceiver = null; + + private FallbackHomeAnimationFactory mActiveAnimationFactory; + private final RecentsWindowManager mRecentsWindowManager; + + private final boolean mRunningOverHome; + + private final Matrix mTmpMatrix = new Matrix(); + private float mMaxLauncherScale = 1; + + private boolean mAppCanEnterPip; + + public RecentsWindowSwipeHandler(Context context, TaskAnimationManager taskAnimationManager, + RecentsAnimationDeviceState deviceState, RotationTouchHelper rotationTouchHelper, + RecentsWindowManager recentsWindowManager, GestureState gestureState, long touchTimeMs, + boolean continuingLastGesture, InputConsumerController inputConsumer, + MSDLPlayerWrapper msdlPlayerWrapper) { + super(context, taskAnimationManager, deviceState, rotationTouchHelper, gestureState, + touchTimeMs, continuingLastGesture, inputConsumer, msdlPlayerWrapper); + + mRecentsWindowManager = recentsWindowManager; + mRunningOverHome = mGestureState.getRunningTask() != null + && mGestureState.getRunningTask().isHomeTask(); + + initTransformParams(); + } + + @Override + public void onRecentsAnimationStart(RecentsAnimationController controller, + RecentsAnimationTargets targets, @Nullable TransitionInfo transitionInfo) { + super.onRecentsAnimationStart(controller, targets, transitionInfo); + initTransformParams(); + } + + private void initTransformParams() { + if (mActiveAnimationFactory != null) { + mActiveAnimationFactory.initTransformParams(); + return; + } + runActionOnRemoteHandles(remoteTargetHandle -> + remoteTargetHandle.getTransformParams().setHomeBuilderProxy( + RecentsWindowSwipeHandler.this::updateHomeActivityTransformDuringSwipeUp)); + } + + @Override + protected void initTransitionEndpoints(DeviceProfile dp) { + super.initTransitionEndpoints(dp); + if (mRunningOverHome) { + // Full screen scale should be independent of remote target handle + mMaxLauncherScale = 1 / mRemoteTargetHandles[0].getTaskViewSimulator() + .getFullScreenScale(); + } + } + + @UiThread + @Override + protected void animateGestureEnd( + float startShift, + float endShift, + long duration, + @NonNull Interpolator interpolator, + @NonNull GestureState.GestureEndTarget endTarget, + @NonNull PointF velocityPxPerMs) { + boolean fromHomeToHome = mRunningOverHome + && endTarget == GestureState.GestureEndTarget.HOME; + if (fromHomeToHome) { + mRecentsWindowManager.startHome(/* finishRecentsAnimation= */ false); + } + super.animateGestureEnd( + startShift, + endShift, + fromHomeToHome ? 0 : duration, + interpolator, + endTarget, + velocityPxPerMs); + } + + private void updateHomeActivityTransformDuringSwipeUp(SurfaceProperties builder, + RemoteAnimationTarget app, TransformParams params) { + if (mActiveAnimationFactory != null) { + return; + } + setHomeScaleAndAlpha( + builder, + app, + mCurrentShift.value, + mRunningOverHome ? Utilities.boundToRange(1 - mCurrentShift.value, 0, 1) : 0f); + } + + private void setHomeScaleAndAlpha(SurfaceProperties builder, + RemoteAnimationTarget app, float verticalShift, float alpha) { + if (app.windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME) { + return; + } + float scale = Utilities.mapRange(verticalShift, 1, mMaxLauncherScale); + mTmpMatrix.setScale(scale, scale, + app.localBounds.exactCenterX(), app.localBounds.exactCenterY()); + builder.setMatrix(mTmpMatrix).setAlpha(alpha); + builder.setShow(); + } + + @Override + protected HomeAnimationFactory createHomeAnimationFactory( + List launchCookies, + long duration, + boolean isTargetTranslucent, + boolean appCanEnterPip, + RemoteAnimationTarget runningTaskTarget, + @Nullable TaskView targetTaskView) { + mAppCanEnterPip = appCanEnterPip; + if (appCanEnterPip) { + return new FallbackPipToHomeAnimationFactory(); + } + mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration); + startHomeIntent( + mActiveAnimationFactory, runningTaskTarget, "RecentsWindowSwipeHandler-home"); + return mActiveAnimationFactory; + } + + private void startHomeIntent( + @Nullable FallbackHomeAnimationFactory gestureContractAnimationFactory, + @Nullable RemoteAnimationTarget runningTaskTarget, + @NonNull String reason) { + ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0); + Intent intent = new Intent(mGestureState.getHomeIntent()); + if (gestureContractAnimationFactory != null && runningTaskTarget != null) { + gestureContractAnimationFactory.addGestureContract(intent, runningTaskTarget.taskInfo); + } + startHomeIntentSafely(mContext, intent, options.toBundle(), reason); + } + + @Override + protected void finishRecentsControllerToHome(Runnable callback) { + final Runnable recentsCallback; + // TODO(b/404866791): check if this is actually necessary for this recents-in-window class + if (mAppCanEnterPip) { + // Make sure Launcher is resumed after auto-enter-pip transition to actually trigger + // the PiP task appearing. + recentsCallback = () -> { + callback.run(); + mRecentsWindowManager.startHome(); + }; + } else { + recentsCallback = callback; + } + if (mRecentsView != null) { + mRecentsView.cleanupRemoteTargets(); + } + mRecentsAnimationController.finish( + true /* toRecents */, recentsCallback, true /* sendUserLeaveHint */); + } + + @Override + protected void switchToScreenshot() { + if (mRunningOverHome) { + // When the current task is home, then we don't need to capture anything + mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + } else { + super.switchToScreenshot(); + } + } + + @Override + protected void notifyGestureAnimationStartToRecents() { + if (mRunningOverHome) { + if (DisplayController.getNavigationMode(mContext).hasGestures && mRecentsView != null) { + mRecentsView.onGestureAnimationStartOnHome( + mGestureState.getRunningTask().getPlaceholderGroupedTaskInfo( + /* splitTaskIds = */ null)); + } + } else { + super.notifyGestureAnimationStartToRecents(); + } + } + + private class FallbackPipToHomeAnimationFactory extends HomeAnimationFactory { + @NonNull + @Override + public AnimatorPlaybackController createActivityAnimationToHome() { + // copied from {@link LauncherSwipeHandlerV2.LauncherHomeAnimationFactory} + long accuracy = 2 * Math.max(mDp.getDeviceProperties().getWidthPx(), mDp.getDeviceProperties().getHeightPx()); + return mRecentsWindowManager + .getStateManager() + .createAnimationToNewWorkspace( + RecentsState.HOME, accuracy, StateAnimationConfig.SKIP_ALL_ANIMATIONS); + } + } + + private class FallbackHomeAnimationFactory extends HomeAnimationFactory + implements Consumer { + private final Rect mTempRect = new Rect(); + + private final TransformParams mTransformParams = new TransformParams(); + private final AnimatedFloat mHomeAlpha = new AnimatedFloat(this::updateAppTransforms); + private final AnimatedFloat mVerticalShiftForScale = + new AnimatedFloat(this::updateAppTransforms); + private final AnimatedFloat mRecentsAlpha = new AnimatedFloat(this::updateAppTransforms); + + private final RectF mTargetRect = new RectF(); + private SurfaceControl mSurfaceControl; + + private boolean mAnimationFinished; + private Message mOnFinishCallback; + + private final long mDuration; + + private RectFSpringAnim mSpringAnim; + FallbackHomeAnimationFactory(long duration) { + mDuration = duration; + + if (mRunningOverHome) { + mVerticalShiftForScale.value = mCurrentShift.value; + } + mRecentsAlpha.value = 1; + mHomeAlpha.value = 0; + + initTransformParams(); + } + + @NonNull + @Override + public RectF getWindowTargetRect() { + if (mTargetRect.isEmpty()) { + mTargetRect.set(super.getWindowTargetRect()); + } + return mTargetRect; + } + + @NonNull + @Override + public AnimatorPlaybackController createActivityAnimationToHome() { + // TODO(b/377678992): Implement a new AtomicAnimationFactory for RecentsWindowManager + PendingAnimation pa = new PendingAnimation(mDuration); + pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCELERATE); + pa.setFloat(mHomeAlpha, AnimatedFloat.VALUE, 1, ACCELERATE); + pa.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + mRecentsWindowManager + .getStateManager() + .goToState(RecentsState.HOME, false); + } + }); + return pa.createPlaybackController(); + } + + @Override + public void playAtomicAnimation(float velocity) { + if (!mRunningOverHome) { + return; + } + // Spring back launcher scale + new SpringAnimationBuilder(mContext) + .setStartValue(mVerticalShiftForScale.value) + .setEndValue(0) + .setStartVelocity(-velocity / mTransitionDragLength) + .setMinimumVisibleChange(1f / mDp.getDeviceProperties().getHeightPx()) + .setDampingRatio(0.6f) + .setStiffness(800) + .build(mVerticalShiftForScale, AnimatedFloat.VALUE) + .start(); + } + + @Override + public void setAnimation(RectFSpringAnim anim) { + mSpringAnim = anim; + mSpringAnim.addAnimatorListener(forEndCallback(this::onRectAnimationEnd)); + } + + private void initTransformParams() { + runActionOnRemoteHandles(remoteTargetHandle -> + remoteTargetHandle.getTransformParams().setHomeBuilderProxy( + FallbackHomeAnimationFactory.this + ::updateHomeActivityTransformDuringHomeAnim)); + + mTransformParams.setHomeBuilderProxy(FallbackHomeAnimationFactory.this + ::updateHomeActivityTransformDuringHomeAnim); + mTransformParams.setTargetSet(mRecentsAnimationTargets); + } + + private void updateRecentsActivityTransformDuringHomeAnim(SurfaceProperties builder, + RemoteAnimationTarget app, TransformParams params) { + if (app.mode != mRecentsAnimationTargets.targetMode) { + return; + } + builder.setAlpha(mRecentsAlpha.value); + } + + private void updateAppTransforms() { + mTransformParams.applySurfaceParams( + mTransformParams.createSurfaceParams(FallbackHomeAnimationFactory.this + ::updateRecentsActivityTransformDuringHomeAnim)); + } + + private void updateHomeActivityTransformDuringHomeAnim(SurfaceProperties builder, + RemoteAnimationTarget app, TransformParams params) { + setHomeScaleAndAlpha(builder, app, mVerticalShiftForScale.value, mHomeAlpha.value); + } + + private void onRectAnimationEnd() { + mAnimationFinished = true; + maybeSendEndMessage(); + } + + private void maybeSendEndMessage() { + if (mAnimationFinished && mOnFinishCallback != null) { + try { + mOnFinishCallback.replyTo.send(mOnFinishCallback); + } catch (RemoteException e) { + Log.e(TAG, "Error sending icon position", e); + } + } + } + + @Override + public void accept(Message msg) { + try { + Bundle data = msg.getData(); + RectF position = data.getParcelable(EXTRA_ICON_POSITION); + if (!position.isEmpty()) { + mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE); + mTargetRect.set(position); + if (mSpringAnim != null) { + mSpringAnim.onTargetPositionChanged(); + } + } + mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK); + maybeSendEndMessage(); + } catch (Exception e) { + // Ignore + } + } + + @Override + public void update(RectF currentRect, float progress, float radius, int overlayAlpha) { + if (mSurfaceControl != null) { + currentRect.roundOut(mTempRect); + Transaction t = new Transaction(); + try { + t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0); + t.apply(); + } catch (RuntimeException e) { + // Ignore + } + } + } + + private void addGestureContract(Intent intent, RunningTaskInfo runningTaskInfo) { + if (mRunningOverHome || runningTaskInfo == null) { + return; + } + + TaskKey key = new TaskKey(runningTaskInfo); + if (key.getComponent() != null) { + if (sMessageReceiver == null) { + sMessageReceiver = new StaticMessageReceiver(); + } + + Bundle gestureNavContract = new Bundle(); + gestureNavContract.putBoolean(EXTRA_ENABLE_GESTURE_CONTRACT, !mIsSwipeForSplit); + gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent()); + gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId)); + gestureNavContract.putParcelable( + EXTRA_REMOTE_CALLBACK, sMessageReceiver.newCallback(this)); + intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract); + } + } + } + + private static class StaticMessageReceiver implements Handler.Callback { + + private final Messenger mMessenger = + new Messenger(new Handler(Looper.getMainLooper(), this)); + + private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID()); + private WeakReference> mCurrentCallback = new WeakReference<>(null); + + public Message newCallback(Consumer callback) { + mCurrentUID = new ParcelUuid(UUID.randomUUID()); + mCurrentCallback = new WeakReference<>(callback); + + Message msg = Message.obtain(); + msg.replyTo = mMessenger; + msg.obj = mCurrentUID; + return msg; + } + + @Override + public boolean handleMessage(@NonNull Message message) { + if (mCurrentUID.equals(message.obj)) { + Consumer consumer = mCurrentCallback.get(); + if (consumer != null) { + consumer.accept(message); + return true; + } + } + return false; + } + } +} diff --git a/quickstep/src/com/android/quickstep/input/QuickstepKeyGestureEventsManager.kt b/quickstep/src/com/android/quickstep/input/QuickstepKeyGestureEventsManager.kt new file mode 100644 index 0000000000..9d4e00d636 --- /dev/null +++ b/quickstep/src/com/android/quickstep/input/QuickstepKeyGestureEventsManager.kt @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.input + +import android.app.PendingIntent +import android.content.Context +import android.hardware.input.InputManager +import android.hardware.input.InputManager.KeyGestureEventHandler +import android.hardware.input.KeyGestureEvent +import android.hardware.input.KeyGestureEvent.ACTION_GESTURE_COMPLETE +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER +import android.net.Uri +import android.os.IBinder +import android.provider.Settings +import android.provider.Settings.Secure.USER_SETUP_COMPLETE +import android.util.Log +import androidx.annotation.VisibleForTesting +import com.android.launcher3.util.SettingsCache +import com.android.launcher3.util.SettingsCache.OnChangeListener +import com.android.quickstep.input.QuickstepKeyGestureEventsManager.OverviewGestureHandler.OverviewType.ALT_TAB +import com.android.quickstep.input.QuickstepKeyGestureEventsManager.OverviewGestureHandler.OverviewType.UNDEFINED +import com.android.window.flags.Flags + +/** + * Manages subscription and unsubscription to launcher's key gesture events, e.g. all apps and + * recents (incl. alt + tab). + */ +class QuickstepKeyGestureEventsManager(context: Context) { + private val settingsCache = SettingsCache.INSTANCE[context] + @VisibleForTesting + val onUserSetupCompleteListener = OnChangeListener { isUserSetupCompleted = it } + private val inputManager = requireNotNull(context.getSystemService(InputManager::class.java)) + private var allAppsPendingIntent: PendingIntent? = null + private var overviewGestureHandler: OverviewGestureHandler? = null + private var isUserSetupCompleted: Boolean = + settingsCache.getValue(USER_SETUP_COMPLETE_URI, /* defaultValue= */ 0) + + init { + settingsCache.register(USER_SETUP_COMPLETE_URI, onUserSetupCompleteListener) + } + + @VisibleForTesting + val allAppsKeyGestureEventHandler = + object : KeyGestureEventHandler { + override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) { + if (!Flags.grantManageKeyGesturesToRecents()) { + return + } + if (!isUserSetupCompleted) { + return + } + + if (event.keyGestureType != KEY_GESTURE_TYPE_ALL_APPS) { + Log.e(TAG, "Ignore unsupported key gesture event type: ${event.keyGestureType}") + return + } + + // Ignore the display ID from the KeyGestureEvent as we will use the focus display + // from the SysUi proxy as the source of truth. + allAppsPendingIntent?.send() + } + } + @VisibleForTesting + val overviewKeyGestureEventHandler = + object : KeyGestureEventHandler { + override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) { + if (!Flags.grantManageKeyGesturesToRecents()) { + return + } + if (!isUserSetupCompleted) { + return + } + + val handler = overviewGestureHandler ?: return + when (event.keyGestureType) { + KEY_GESTURE_TYPE_RECENT_APPS -> { + if (event.action == ACTION_GESTURE_COMPLETE && !event.isCancelled) { + handler.showOverview(UNDEFINED) + } + } + KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER -> { + if (event.action == KeyGestureEvent.ACTION_GESTURE_START) { + handler.showOverview(ALT_TAB) + } else { + handler.hideOverview(ALT_TAB) + } + } + else -> { + Log.e( + TAG, + "Ignore unsupported overview key gesture event type: " + + event.keyGestureType, + ) + } + } + } + } + + /** Registers the all apps key gesture events. */ + fun registerAllAppsKeyGestureEvent(allAppsPendingIntent: PendingIntent) { + if (Flags.grantManageKeyGesturesToRecents()) { + this.allAppsPendingIntent = allAppsPendingIntent + inputManager.registerKeyGestureEventHandler( + listOf(KEY_GESTURE_TYPE_ALL_APPS), + allAppsKeyGestureEventHandler, + ) + } + } + + /** Unregisters the all apps key gesture events. */ + fun unregisterAllAppsKeyGestureEvent() { + if (Flags.grantManageKeyGesturesToRecents()) { + inputManager.unregisterKeyGestureEventHandler(allAppsKeyGestureEventHandler) + } + } + + /** Registers the overview key gesture events. */ + fun registerOverviewKeyGestureEvent(overviewGestureHandler: OverviewGestureHandler) { + if (Flags.grantManageKeyGesturesToRecents()) { + this.overviewGestureHandler = overviewGestureHandler + inputManager.registerKeyGestureEventHandler( + listOf(KEY_GESTURE_TYPE_RECENT_APPS, KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER), + overviewKeyGestureEventHandler, + ) + } + } + + /** Unregisters the overview key gesture events. */ + fun unregisterOverviewKeyGestureEvent() { + if (Flags.grantManageKeyGesturesToRecents()) { + inputManager.unregisterKeyGestureEventHandler(overviewKeyGestureEventHandler) + } + } + + fun onDestroy() { + settingsCache.unregister(USER_SETUP_COMPLETE_URI, onUserSetupCompleteListener) + unregisterOverviewKeyGestureEvent() + unregisterAllAppsKeyGestureEvent() + } + + /** Callbacks for overview events, including alt + tab. */ + interface OverviewGestureHandler { + enum class OverviewType { + UNDEFINED, + ALT_TAB, + HOME, + } + + /** Shows the overview UI with [type]. */ + fun showOverview(type: OverviewType) + + /** Hides the overview UI with [type]. */ + fun hideOverview(type: OverviewType) + } + + private companion object { + const val TAG = "KeyGestureEventsHandler" + val USER_SETUP_COMPLETE_URI: Uri = Settings.Secure.getUriFor(USER_SETUP_COMPLETE) + } +} diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java index ec6efcb440..91435d92fd 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java @@ -29,9 +29,9 @@ import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.android.launcher3.R; -import com.android.quickstep.GestureState; import com.android.quickstep.InputConsumer; import com.android.quickstep.RecentsAnimationDeviceState; +import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.MotionPauseDetector; import com.android.systemui.shared.system.InputMonitorCompat; @@ -47,7 +47,7 @@ public class AccessibilityInputConsumer extends DelegateInputConsumer { private final VelocityTracker mVelocityTracker; private final MotionPauseDetector mMotionPauseDetector; private final RecentsAnimationDeviceState mDeviceState; - private final GestureState mGestureState; + private final RotationTouchHelper mRotationHelper; private final float mMinGestureDistance; private final float mMinFlingVelocity; @@ -56,16 +56,21 @@ public class AccessibilityInputConsumer extends DelegateInputConsumer { private float mDownY; private float mTotalY; - public AccessibilityInputConsumer(Context context, RecentsAnimationDeviceState deviceState, - GestureState gestureState, InputConsumer delegate, InputMonitorCompat inputMonitor) { - super(delegate, inputMonitor); + public AccessibilityInputConsumer( + Context context, + int displayId, + RecentsAnimationDeviceState deviceState, + InputConsumer delegate, + InputMonitorCompat inputMonitor, + RotationTouchHelper rotationTouchHelper) { + super(displayId, delegate, inputMonitor); mContext = context; mVelocityTracker = VelocityTracker.obtain(); mMinGestureDistance = context.getResources() .getDimension(R.dimen.accessibility_gesture_min_swipe_distance); mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); mDeviceState = deviceState; - mGestureState = gestureState; + mRotationHelper = rotationTouchHelper; mMotionPauseDetector = new MotionPauseDetector(context); } @@ -102,8 +107,8 @@ public class AccessibilityInputConsumer extends DelegateInputConsumer { case ACTION_POINTER_DOWN: { if (mState == STATE_INACTIVE) { int pointerIndex = ev.getActionIndex(); - if (mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, - pointerIndex) && mDelegate.allowInterceptByParent()) { + if (mRotationHelper.isInSwipeUpTouchRegion(ev, pointerIndex) + && mDelegate.allowInterceptByParent()) { setActive(ev); mActivePointerId = ev.getPointerId(pointerIndex); diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java index 222ccd3dd1..365014d331 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java @@ -95,7 +95,7 @@ public class AssistantInputConsumer extends DelegateInputConsumer { InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState, MotionEvent startEvent) { - super(delegate, inputMonitor); + super(gestureState.getDisplayId(), delegate, inputMonitor); final Resources res = context.getResources(); mContext = context; mDragDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold); diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java new file mode 100644 index 0000000000..86d71907f0 --- /dev/null +++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java @@ -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. + */ + +package com.android.quickstep.inputconsumers; + +import static android.view.MotionEvent.INVALID_POINTER_ID; + +import android.content.Context; +import android.graphics.PointF; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.annotation.Nullable; + +import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; +import com.android.launcher3.taskbar.bubbles.BubbleControllers; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; +import com.android.launcher3.testing.TestLogging; +import com.android.launcher3.testing.shared.TestProtocol; +import com.android.quickstep.InputConsumer; +import com.android.systemui.shared.system.InputMonitorCompat; + +/** + * Listens for touch events on the bubble bar. + */ +// TODO(b/385928447): remove debug logs with Log.d +public class BubbleBarInputConsumer implements InputConsumer { + + private static final String TAG = "BubbleBarInputConsumer"; + + private final BubbleStashController mBubbleStashController; + private final BubbleBarViewController mBubbleBarViewController; + @Nullable + private final BubbleBarSwipeController mBubbleBarSwipeController; + private final InputMonitorCompat mInputMonitorCompat; + + private boolean mPilfered; + private boolean mPassedTouchSlop; + private boolean mStashedOrCollapsedOnDown; + + private final int mTouchSlop; + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + + private final int mDisplayId; + + private long mDownTime; + private final long mTimeForLongPress; + private int mActivePointerId = INVALID_POINTER_ID; + + public BubbleBarInputConsumer( + Context context, + int displayId, + BubbleControllers bubbleControllers, + InputMonitorCompat inputMonitorCompat) { + mDisplayId = displayId; + mBubbleStashController = bubbleControllers.bubbleStashController; + mBubbleBarViewController = bubbleControllers.bubbleBarViewController; + mBubbleBarSwipeController = bubbleControllers.bubbleBarSwipeController.orElse(null); + + mInputMonitorCompat = inputMonitorCompat; + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mTimeForLongPress = ViewConfiguration.getLongPressTimeout(); + } + + @Override + public int getType() { + return TYPE_BUBBLE_BAR; + } + + @Override + public int getDisplayId() { + return mDisplayId; + } + + @Override + public void onMotionEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mDownTime = System.currentTimeMillis(); + mActivePointerId = ev.getPointerId(0); + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + mStashedOrCollapsedOnDown = mBubbleStashController.isStashed() || isCollapsed(); + Log.d(TAG, + "ACTION_DOWN stashedOrCollapsed=" + mStashedOrCollapsedOnDown + " downPos=" + + mDownPos); + if (mBubbleBarSwipeController != null) { + mBubbleBarSwipeController.start(); + } + break; + case MotionEvent.ACTION_MOVE: + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == INVALID_POINTER_ID) { + Log.d(TAG, "ACTION_MOVE skip, invalid pointer id"); + break; + } + mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); + + float dX = mLastPos.x - mDownPos.x; + float dY = mLastPos.y - mDownPos.y; + if (!mPassedTouchSlop) { + mPassedTouchSlop = Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop; + if (mPassedTouchSlop) { + Log.d(TAG, "ACTION_MOVE passed touch slop pos=" + mLastPos); + } + } + if (mBubbleBarSwipeController != null) { + mBubbleBarSwipeController.swipeTo(dY); + if (!mPilfered && mBubbleBarSwipeController.isSwipeGesture()) { + Log.d(TAG, "ACTION_MOVE swipe gesture, pilfering"); + mPilfered = true; + // Bubbles is handling the swipe so make sure no one else gets it. + TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers"); + mInputMonitorCompat.pilferPointers(); + } + } + break; + case MotionEvent.ACTION_UP: + long tapTime = System.currentTimeMillis() - mDownTime; + boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null + && mBubbleBarSwipeController.isSwipeGesture(); + // Anything less than a long-press is a tap + boolean isWithinTapTime = tapTime <= mTimeForLongPress; + Log.d(TAG, "ACTION_UP swipeUp=" + swipeUpOnBubbleHandle + " isInTapTime=" + + isWithinTapTime + " tapTime=" + tapTime + " passedTouchSlop=" + + mPassedTouchSlop + " stashedOrCollapsedOnDown=" + + mStashedOrCollapsedOnDown); + if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop + && mStashedOrCollapsedOnDown) { + Log.d(TAG, "ACTION_UP showing bubble bar"); + // Taps on the handle / collapsed state should open the bar + mBubbleStashController.showBubbleBar( + /* expandBubbles= */ true, /* bubbleBarGesture= */ true); + } else { + Log.d(TAG, "ACTION_UP nothing to do"); + } + break; + case MotionEvent.ACTION_CANCEL: + Log.d(TAG, "ACTION_CANCEL"); + break; + } + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + cleanupAfterMotionEvent(); + } + } + + private void cleanupAfterMotionEvent() { + Log.d(TAG, "cleaning up passedSlop=" + mPassedTouchSlop + " pilfered=" + mPilfered); + if (mBubbleBarSwipeController != null) { + mBubbleBarSwipeController.finish(); + } + mPassedTouchSlop = false; + mPilfered = false; + mDownTime = 0; + } + + private boolean isCollapsed() { + return mBubbleStashController.isBubbleBarVisible() + && !mBubbleBarViewController.isExpanded(); + } + + /** + * Returns whether the event is occurring on a visible bubble bar or the bar handle. + */ + public static boolean isEventOnBubbles(TaskbarActivityContext tac, MotionEvent ev) { + if (tac == null || !tac.isBubbleBarEnabled()) { + return false; + } + BubbleControllers controllers = tac.getBubbleControllers(); + if (controllers == null || !controllers.bubbleBarViewController.hasBubbles()) { + return false; + } + if (controllers.bubbleStashController.isStashed() + && controllers.bubbleStashedHandleViewController.isPresent()) { + return controllers.bubbleStashedHandleViewController.get().isEventOverHandle(ev); + } else if (controllers.bubbleBarViewController.isBubbleBarVisible()) { + return controllers.bubbleBarViewController.isEventOverBubbleBar(ev); + } + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java index 5557639b1c..0b1a6c4640 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java @@ -5,7 +5,7 @@ import android.view.MotionEvent; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.quickstep.InputConsumer; -import com.android.quickstep.util.ActiveGestureLog; +import com.android.quickstep.util.ActiveGestureProtoLogProxy; import com.android.systemui.shared.system.InputMonitorCompat; public abstract class DelegateInputConsumer implements InputConsumer { @@ -17,14 +17,23 @@ public abstract class DelegateInputConsumer implements InputConsumer { protected final InputConsumer mDelegate; protected final InputMonitorCompat mInputMonitor; + private final int mDisplayId; + protected int mState; - public DelegateInputConsumer(InputConsumer delegate, InputMonitorCompat inputMonitor) { + public DelegateInputConsumer( + int displayId, InputConsumer delegate, InputMonitorCompat inputMonitor) { + mDisplayId = displayId; mDelegate = delegate; mInputMonitor = inputMonitor; mState = STATE_INACTIVE; } + @Override + public int getDisplayId() { + return mDisplayId; + } + @Override public InputConsumer getActiveConsumerInHierarchy() { if (mState == STATE_ACTIVE) { @@ -57,8 +66,7 @@ public abstract class DelegateInputConsumer implements InputConsumer { } protected void setActive(MotionEvent ev) { - ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(getDelegatorName()) - .append(" became active")); + ActiveGestureProtoLogProxy.logInputConsumerBecameActive(getDelegatorName()); mState = STATE_ACTIVE; TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers"); diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java index f2643641e0..0489ef9856 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java @@ -24,7 +24,6 @@ import static com.android.launcher3.util.VelocityUtils.PX_PER_MS; import static com.android.quickstep.AbsSwipeUpHandler.MIN_PROGRESS_FOR_OVERVIEW; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; import android.animation.Animator; @@ -38,6 +37,7 @@ import android.graphics.PointF; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.VelocityTracker; +import android.window.TransitionInfo; import com.android.app.animation.Interpolators; import com.android.launcher3.R; @@ -53,12 +53,12 @@ import com.android.quickstep.RecentsAnimationController; import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RemoteAnimationTargets; +import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.TaskAnimationManager; import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.TransformParams.BuilderProxy; import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputMonitorCompat; import java.util.HashMap; @@ -84,7 +84,7 @@ public class DeviceLockedInputConsumer implements InputConsumer, getFlagForIndex(1, "STATE_HANDLER_INVALIDATED"); private final Context mContext; - private final RecentsAnimationDeviceState mDeviceState; + private final RotationTouchHelper mRotationTouchHelper; private final TaskAnimationManager mTaskAnimationManager; private final GestureState mGestureState; private final float mTouchSlopSquared; @@ -108,18 +108,22 @@ public class DeviceLockedInputConsumer implements InputConsumer, private RecentsAnimationController mRecentsAnimationController; - public DeviceLockedInputConsumer(Context context, RecentsAnimationDeviceState deviceState, - TaskAnimationManager taskAnimationManager, GestureState gestureState, - InputMonitorCompat inputMonitorCompat) { + public DeviceLockedInputConsumer( + Context context, + RecentsAnimationDeviceState deviceState, + TaskAnimationManager taskAnimationManager, + GestureState gestureState, + InputMonitorCompat inputMonitorCompat, + RotationTouchHelper rotationTouchHelper) { mContext = context; - mDeviceState = deviceState; mTaskAnimationManager = taskAnimationManager; mGestureState = gestureState; - mTouchSlopSquared = mDeviceState.getSquaredTouchSlop(); + mTouchSlopSquared = deviceState.getSquaredTouchSlop(); mTransformParams = new TransformParams(); mInputMonitorCompat = inputMonitorCompat; mMaxTranslationY = context.getResources().getDimensionPixelSize( R.dimen.device_locked_y_offset); + mRotationTouchHelper = rotationTouchHelper; // Do not use DeviceProfile as the user data might be locked mDisplaySize = DisplayController.INSTANCE.get(context).getInfo().currentSize; @@ -137,6 +141,11 @@ public class DeviceLockedInputConsumer implements InputConsumer, return TYPE_DEVICE_LOCKED; } + @Override + public int getDisplayId() { + return mGestureState.getDisplayId(); + } + @Override public void onMotionEvent(MotionEvent ev) { if (mVelocityTracker == null) { @@ -154,7 +163,7 @@ public class DeviceLockedInputConsumer implements InputConsumer, if (!mThresholdCrossed) { // Cancel interaction in case of multi-touch interaction int ptrIdx = ev.getActionIndex(); - if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx)) { + if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) { int action = ev.getAction(); ev.setAction(ACTION_CANCEL); finishTouchTracking(ev); @@ -213,15 +222,13 @@ public class DeviceLockedInputConsumer implements InputConsumer, // This will come back and cancel the interaction. startHomeIntentSafely(mContext, mGestureState.getHomeIntent(), null, TAG); mHomeLaunched = true; - } else if (ENABLE_SHELL_TRANSITIONS) { - if (mTaskAnimationManager.getCurrentCallbacks() != null) { - if (mRecentsAnimationController != null) { - finishRecentsAnimationForShell(dismissTask); - } else { - // the transition of recents animation hasn't started, wait for it - mCancelWhenRecentsStart = true; - mDismissTask = dismissTask; - } + } else if (mTaskAnimationManager.getCurrentCallbacks() != null) { + if (mRecentsAnimationController != null) { + finishRecentsAnimationForShell(dismissTask); + } else { + // the transition of recents animation hasn't started, wait for it + mCancelWhenRecentsStart = true; + mDismissTask = dismissTask; } } mStateCallback.setState(STATE_HANDLER_INVALIDATED); @@ -252,7 +259,7 @@ public class DeviceLockedInputConsumer implements InputConsumer, @Override public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { + RecentsAnimationTargets targets, TransitionInfo transitionInfo) { mRecentsAnimationController = controller; mTransformParams.setTargetSet(targets); applyTransform(); @@ -278,9 +285,7 @@ public class DeviceLockedInputConsumer implements InputConsumer, } private void endRemoteAnimation() { - if (mHomeLaunched) { - ActivityManagerWrapper.getInstance().cancelRecentsAnimation(false); - } else if (mRecentsAnimationController != null) { + if (!mHomeLaunched && mRecentsAnimationController != null) { mRecentsAnimationController.finishController( false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java index 1d00e533f5..5249b879c6 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java @@ -16,23 +16,72 @@ package com.android.quickstep.inputconsumers; +import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_NAV_HANDLE; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_OMNI_RUNNABLE; + import android.content.Context; +import android.os.SystemClock; +import android.util.Log; +import android.view.ViewConfiguration; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; -import com.android.launcher3.R; -import com.android.launcher3.util.ResourceBasedOverride; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherComponentProvider; +import com.android.launcher3.logging.InstanceId; +import com.android.launcher3.logging.InstanceIdSequence; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.util.VibratorWrapper; +import com.android.quickstep.DeviceConfigWrapper; import com.android.quickstep.NavHandle; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.util.ContextualSearchHapticManager; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.quickstep.util.ContextualSearchStateManager; + +import javax.inject.Inject; /** * Class for extending nav handle long press behavior */ -public class NavHandleLongPressHandler implements ResourceBasedOverride { +public class NavHandleLongPressHandler { + + private static final String TAG = "NavHandleLongPressHandler"; + protected final VibratorWrapper mVibratorWrapper; + protected final ContextualSearchHapticManager mContextualSearchHapticManager; + protected final ContextualSearchInvoker mContextualSearchInvoker; + protected final StatsLogManager mStatsLogManager; + private boolean mPendingInvocation; + protected final TopTaskTracker mTopTaskTracker; + private final ContextualSearchStateManager mContextualSearchStateManager; + + @Inject + public NavHandleLongPressHandler(@ApplicationContext Context context, + VibratorWrapper vibratorWrapper, + ContextualSearchHapticManager hapticManager, + TopTaskTracker topTaskTracker, + StatsLogManager.StatsLogManagerFactory logManagerFactory, + ContextualSearchStateManager contextualSearchStateManager, + ContextualSearchInvoker contextualSearchInvoker) { + mStatsLogManager = logManagerFactory.create(context); + mVibratorWrapper = vibratorWrapper; + mContextualSearchHapticManager = hapticManager; + mContextualSearchInvoker = contextualSearchInvoker; + mTopTaskTracker = topTaskTracker; + mContextualSearchStateManager = contextualSearchStateManager; + } /** Creates NavHandleLongPressHandler as specified by overrides */ public static NavHandleLongPressHandler newInstance(Context context) { - return Overrides.getObject(NavHandleLongPressHandler.class, context, - R.string.nav_handle_long_press_handler_class); + return LauncherComponentProvider.get(context).getNavHandleLongPressHandler(); + } + + protected boolean isContextualSearchEntrypointEnabled(NavHandle navHandle) { + return DeviceConfigWrapper.get().getEnableLongPressNavHandle(); } /** @@ -46,8 +95,48 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * * @param navHandle to handle this long press */ - public @Nullable Runnable getLongPressRunnable(NavHandle navHandle) { - return null; + @Nullable + @VisibleForTesting + final Runnable getLongPressRunnable(NavHandle navHandle, int displayId) { + if (!isContextualSearchEntrypointEnabled(navHandle)) { + Log.i(TAG, "Contextual Search invocation failed: entry point disabled"); + mVibratorWrapper.cancelVibrate(); + return null; + } + + if (!mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) { + Log.i(TAG, "Contextual Search invocation failed: precondition not satisfied"); + mVibratorWrapper.cancelVibrate(); + return null; + } + + mPendingInvocation = true; + Log.i(TAG, "Contextual Search invocation: invocation runnable created"); + InstanceId instanceId = new InstanceIdSequence().newInstanceId(); + mStatsLogManager.logger().withInstanceId(instanceId).log( + LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE); + long startTimeMillis = SystemClock.elapsedRealtime(); + return () -> { + mStatsLogManager.latencyLogger().withInstanceId(instanceId).withLatency( + SystemClock.elapsedRealtime() - startTimeMillis).log( + LAUNCHER_LATENCY_OMNI_RUNNABLE); + if (mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + ENTRYPOINT_LONG_PRESS_NAV_HANDLE)) { + Log.i(TAG, "Contextual Search invocation successful"); + + String runningPackage = mTopTaskTracker.getCachedTopTask( + /* filterOnlyVisibleRecents */ true, displayId).getPackageName(); + mStatsLogManager.logger().withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE); + } else { + mVibratorWrapper.cancelVibrate(); + if (DeviceConfigWrapper.get().getAnimateLpnh() + && !DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/160); + } + } + }; } /** @@ -55,7 +144,15 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * * @param navHandle to handle the animation for this touch */ - public void onTouchStarted(NavHandle navHandle) {} + @VisibleForTesting + final void onTouchStarted(NavHandle navHandle) { + mPendingInvocation = false; + if (isContextualSearchEntrypointEnabled(navHandle) + && mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) { + Log.i(TAG, "Contextual Search invocation: touch started"); + startNavBarAnimation(navHandle); + } + } /** * Called when nav handle gesture is finished by the user lifting their finger or the system @@ -64,5 +161,44 @@ public class NavHandleLongPressHandler implements ResourceBasedOverride { * @param navHandle to handle the animation for this touch * @param reason why the touch ended */ - public void onTouchFinished(NavHandle navHandle, String reason) {} + @VisibleForTesting + final void onTouchFinished(NavHandle navHandle, String reason) { + Log.i(TAG, "Contextual Search invocation: touch finished with reason: " + reason); + + if (!DeviceConfigWrapper.get().getShrinkNavHandleOnPress() || !mPendingInvocation) { + mVibratorWrapper.cancelVibrate(); + } + + if (DeviceConfigWrapper.get().getAnimateLpnh()) { + if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ true, /*durationMs*/200); + } else { + navHandle.animateNavBarLongPress( + /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/ 160); + } + } + } + + @VisibleForTesting + final void startNavBarAnimation(NavHandle navHandle) { + mContextualSearchHapticManager.vibrateForSearchHint(); + + if (DeviceConfigWrapper.get().getAnimateLpnh()) { + if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) { + navHandle.animateNavBarLongPress( + /*isTouchDown*/ true, /*shrink*/true, /*durationMs*/200); + } else { + long longPressTimeout; + if (mContextualSearchStateManager.getLPNHDurationMillis().isPresent()) { + longPressTimeout = + mContextualSearchStateManager.getLPNHDurationMillis().get().intValue(); + } else { + longPressTimeout = ViewConfiguration.getLongPressTimeout(); + } + navHandle.animateNavBarLongPress( + /*isTouchDown*/ true, /*shrink*/ false, /*durationMs*/ longPressTimeout); + } + } + } } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java index 848a43afb1..af7c975e0b 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java @@ -19,6 +19,7 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DEEP_PRESS_STASHED_TASKBAR; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_STASHED_TASKBAR; +import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.LogConfig.NAV_HANDLE_LONG_PRESS; @@ -27,7 +28,10 @@ import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.Utilities; +import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.util.DisplayController; import com.android.quickstep.DeviceConfigWrapper; @@ -36,7 +40,7 @@ import com.android.quickstep.InputConsumer; import com.android.quickstep.NavHandle; import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.TopTaskTracker; -import com.android.quickstep.util.AssistStateManager; +import com.android.quickstep.util.ContextualSearchStateManager; import com.android.systemui.shared.system.InputMonitorCompat; /** @@ -47,8 +51,10 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { private static final String TAG = "NavHandleLongPressIC"; private static final boolean DEBUG_NAV_HANDLE = Utilities.isPropertyEnabled( NAV_HANDLE_LONG_PRESS); + // Minimum time between touch down and abandon to log. + @VisibleForTesting static final long MIN_TIME_TO_LOG_ABANDON_MS = 200; - private final NavHandleLongPressHandler mNavHandleLongPressHandler; + private NavHandleLongPressHandler mNavHandleLongPressHandler; private final float mNavHandleWidth; private final float mScreenWidth; @@ -60,30 +66,52 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { private final int mOuterLongPressTimeout; private final boolean mDeepPressEnabled; private final NavHandle mNavHandle; - private final StatsLogManager mStatsLogManager; + private StatsLogManager mStatsLogManager; private final TopTaskTracker mTopTaskTracker; private final GestureState mGestureState; - private MotionEvent mCurrentDownEvent; + private MotionEvent mCurrentDownEvent; // Down event that started the current gesture. + private MotionEvent mCurrentMotionEvent; // Most recent motion event. private boolean mDeepPressLogged; // Whether deep press has been logged for the current touch. - public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate, - InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState, - NavHandle navHandle, GestureState gestureState) { - super(delegate, inputMonitor); + public NavHandleLongPressInputConsumer( + Context context, + InputConsumer delegate, + InputMonitorCompat inputMonitor, + RecentsAnimationDeviceState deviceState, + NavHandle navHandle, + GestureState gestureState) { + super(gestureState.getDisplayId(), delegate, inputMonitor); mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x; mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress(); - int twoStageMultiplier = DeviceConfigWrapper.get().getTwoStageMultiplier(); - AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(context); - if (assistStateManager.getLPNHDurationMillis().isPresent()) { - mLongPressTimeout = assistStateManager.getLPNHDurationMillis().get().intValue(); + ContextualSearchStateManager contextualSearchStateManager = + ContextualSearchStateManager.INSTANCE.get(context); + if (contextualSearchStateManager.getLPNHDurationMillis().isPresent()) { + mLongPressTimeout = + contextualSearchStateManager.getLPNHDurationMillis().get().intValue(); } else { mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); } - mOuterLongPressTimeout = mLongPressTimeout * twoStageMultiplier; - mTouchSlopSquaredOriginal = deviceState.getSquaredTouchSlop(); - mTouchSlopSquared = mTouchSlopSquaredOriginal; - mOuterTouchSlopSquared = mTouchSlopSquared * (twoStageMultiplier * twoStageMultiplier); + float twoStageDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + mOuterLongPressTimeout = (int) (mLongPressTimeout * twoStageDurationMultiplier); + + float gestureNavTouchSlopSquared = deviceState.getSquaredTouchSlop(); + float twoStageSlopMultiplier = + (DeviceConfigWrapper.get().getTwoStageSlopPercentage() / 100f); + float twoStageSlopMultiplierSquared = twoStageSlopMultiplier * twoStageSlopMultiplier; + if (DeviceConfigWrapper.get().getEnableLpnhTwoStages()) { + // For 2 stages, the outer touch slop should match gesture nav. + mTouchSlopSquared = gestureNavTouchSlopSquared * twoStageSlopMultiplierSquared; + mOuterTouchSlopSquared = gestureNavTouchSlopSquared; + } else { + // For single stage, the touch slop should match gesture nav. + mTouchSlopSquared = gestureNavTouchSlopSquared; + // Note: This outer slop is not actually used for single-stage (flag disabled). + mOuterTouchSlopSquared = gestureNavTouchSlopSquared; + } + mTouchSlopSquaredOriginal = mTouchSlopSquared; + mGestureState = gestureState; mGestureState.setIsInExtendedSlopRegion(false); if (DEBUG_NAV_HANDLE) { @@ -106,6 +134,10 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { @Override public void onMotionEvent(MotionEvent ev) { + if (mCurrentMotionEvent != null) { + mCurrentMotionEvent.recycle(); + } + mCurrentMotionEvent = MotionEvent.obtain(ev); if (mDelegate.allowInterceptByParent()) { handleMotionEvent(ev); } else if (MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)) { @@ -182,7 +214,7 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { && !mDeepPressLogged) { // Log deep press even if feature is disabled. String runningPackage = mTopTaskTracker.getCachedTopTask( - /* filterOnlyVisibleRecents */ true).getPackageName(); + /* filterOnlyVisibleRecents */ true, getDisplayId()).getPackageName(); mStatsLogManager.logger().withPackageName(runningPackage).log( mNavHandle.isNavHandleStashedTaskbar() ? LAUNCHER_DEEP_PRESS_STASHED_TASKBAR : LAUNCHER_DEEP_PRESS_NAVBAR); @@ -201,12 +233,13 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { Log.d(TAG, "triggerLongPress"); } String runningPackage = mTopTaskTracker.getCachedTopTask( - /* filterOnlyVisibleRecents */ true).getPackageName(); + /* filterOnlyVisibleRecents */ true, getDisplayId()).getPackageName(); mStatsLogManager.logger().withPackageName(runningPackage).log( mNavHandle.isNavHandleStashedTaskbar() ? LAUNCHER_LONG_PRESS_STASHED_TASKBAR : LAUNCHER_LONG_PRESS_NAVBAR); - Runnable longPressRunnable = mNavHandleLongPressHandler.getLongPressRunnable(mNavHandle); + Runnable longPressRunnable = mNavHandleLongPressHandler.getLongPressRunnable(mNavHandle, + getDisplayId()); if (longPressRunnable == null) { return; } @@ -223,7 +256,18 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { private void cancelLongPress(String reason) { if (DEBUG_NAV_HANDLE) { - Log.d(TAG, "cancelLongPress"); + Log.d(TAG, "cancelLongPress: " + reason); + } + // Log LPNH abandon latency if we didn't trigger but were still prepared to. + if (mCurrentMotionEvent != null && mCurrentDownEvent != null) { + long latencyMs = mCurrentMotionEvent.getEventTime() - mCurrentDownEvent.getEventTime(); + if (mState != STATE_ACTIVE && MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress) + && latencyMs >= MIN_TIME_TO_LOG_ABANDON_MS) { + mStatsLogManager.latencyLogger() + .withInstanceId(new InstanceIdSequence().newInstanceId()) + .withLatency(latencyMs) + .log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } } mGestureState.setIsInExtendedSlopRegion(false); MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress); @@ -250,4 +294,14 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { protected String getDelegatorName() { return "NavHandleLongPressInputConsumer"; } + + @VisibleForTesting + void setNavHandleLongPressHandler(NavHandleLongPressHandler navHandleLongPressHandler) { + mNavHandleLongPressHandler = navHandleLongPressHandler; + } + + @VisibleForTesting + void setStatsLogManager(StatsLogManager statsLogManager) { + mStatsLogManager = statsLogManager; + } } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java index 2db7f8da08..0cd072ba0e 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java @@ -62,9 +62,13 @@ public class OneHandedModeInputConsumer extends DelegateInputConsumer { private boolean mPassedSlop; private boolean mIsStopGesture; - public OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState, - InputConsumer delegate, InputMonitorCompat inputMonitor) { - super(delegate, inputMonitor); + public OneHandedModeInputConsumer( + Context context, + int displayId, + RecentsAnimationDeviceState deviceState, + InputConsumer delegate, + InputMonitorCompat inputMonitor) { + super(displayId, delegate, inputMonitor); mContext = context; mDeviceState = deviceState; mDragDistThreshold = context.getResources().getDimensionPixelSize( diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java index 0d450c6b15..8de604e53b 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java @@ -38,6 +38,7 @@ import android.graphics.PointF; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; +import android.window.TransitionInfo; import androidx.annotation.UiThread; @@ -57,6 +58,7 @@ import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.TaskAnimationManager; +import com.android.quickstep.util.ActiveGestureProtoLogProxy; import com.android.quickstep.util.CachedEventDispatcher; import com.android.quickstep.util.MotionPauseDetector; import com.android.quickstep.util.NavBarPosition; @@ -70,6 +72,9 @@ import java.util.function.Consumer; */ public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer { + private static final String TAG = "OtherActivityInputConsumer"; + private static final boolean DEBUG = true; + public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN"; private static final String UP_EVT = "OtherActivityInputConsumer.UP"; @@ -121,11 +126,18 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC // The callback called upon finishing the recents transition if it was force-canceled private Runnable mForceFinishRecentsTransitionCallback; - public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState, - TaskAnimationManager taskAnimationManager, GestureState gestureState, - boolean isDeferredDownTarget, Consumer onCompleteCallback, - InputMonitorCompat inputMonitorCompat, InputEventReceiver inputEventReceiver, - boolean disableHorizontalSwipe, Factory handlerFactory) { + public OtherActivityInputConsumer( + Context base, + RecentsAnimationDeviceState deviceState, + TaskAnimationManager taskAnimationManager, + GestureState gestureState, + boolean isDeferredDownTarget, + Consumer onCompleteCallback, + InputMonitorCompat inputMonitorCompat, + InputEventReceiver inputEventReceiver, + boolean disableHorizontalSwipe, + Factory handlerFactory, + RotationTouchHelper rotationTouchHelper) { super(base); mDeviceState = deviceState; mNavBarPosition = mDeviceState.getNavBarPosition(); @@ -152,7 +164,7 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture; mStartDisplacement = continuingPreviousGesture ? 0 : -mTouchSlop; mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe; - mRotationTouchHelper = mDeviceState.getRotationTouchHelper(); + mRotationTouchHelper = rotationTouchHelper; } @Override @@ -160,6 +172,11 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC return TYPE_OTHER_ACTIVITY; } + @Override + public int getDisplayId() { + return mGestureState.getDisplayId(); + } + @Override public boolean isConsumerDetachedFromGesture() { return true; @@ -230,6 +247,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC // Start the window animation on down to give more time for launcher to draw if the // user didn't start the gesture over the back button + if (DEBUG) { + Log.d(TAG, "ACTION_DOWN: mIsDeferredDownTarget=" + mIsDeferredDownTarget); + } if (!mIsDeferredDownTarget) { startTouchTrackingForWindowAnimation(ev.getEventTime()); } @@ -284,9 +304,18 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC float horizontalDist = Math.abs(displacementX); float upDist = -displacement; - boolean passedSlop = mGestureState.isTrackpadGesture() - || (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop - && !mGestureState.isInExtendedSlopRegion()); + boolean isTrackpadGesture = mGestureState.isTrackpadGesture(); + float squaredHypot = squaredHypot(displacementX, displacementY); + boolean isInExtendedSlopRegion = mGestureState.isInExtendedSlopRegion(); + boolean passedSlop = isTrackpadGesture + || (squaredHypot >= mSquaredTouchSlop + && !isInExtendedSlopRegion); + if (DEBUG) { + Log.d(TAG, "ACTION_MOVE: passedSlop=" + passedSlop + + " ( " + isTrackpadGesture + + " || (" + squaredHypot + " >= " + mSquaredTouchSlop + + " && " + !isInExtendedSlopRegion + " ))"); + } if (!mPassedSlopOnThisGesture && passedSlop) { mPassedSlopOnThisGesture = true; @@ -306,6 +335,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC boolean isLikelyToStartNewTask = haveNotPassedSlopOnContinuedGesture || swipeWithinQuickSwitchRange; + if (DEBUG) { + Log.d(TAG, "ACTION_MOVE: mPassedPilferInputSlop=" + mPassedPilferInputSlop); + } if (!mPassedPilferInputSlop) { if (passedSlop) { // Horizontal gesture is not allowed in this region @@ -389,11 +421,25 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC private void startTouchTrackingForWindowAnimation(long touchTimeMs) { mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs); + if (mInteractionHandler == null) { + // Can happen e.g. when a display is disconnected, so try to handle gracefully. + Log.d(TAG, "AbsSwipeUpHandler not available for displayId=$focusedDisplayId"); + ActiveGestureProtoLogProxy.logOnAbsSwipeUpHandlerNotAvailable( + mGestureState.getDisplayId()); + return; + } mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished); mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener()); + mMotionPauseDetector.setIsTrackpadGesture(mGestureState.isTrackpadGesture()); mInteractionHandler.initWhenReady( "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation"); + ActiveGestureProtoLogProxy.logGestureStartSwipeHandler( + mInteractionHandler.getClass().getSimpleName()); + if (DEBUG) { + Log.d(TAG, "startTouchTrackingForWindowAnimation: isRecentsAnimationRunning=" + + mTaskAnimationManager.isRecentsAnimationRunning()); + } if (mTaskAnimationManager.isRecentsAnimationRunning()) { mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState); mActiveCallbacks.removeListener(mCleanupHandler); @@ -422,6 +468,11 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC */ private void finishTouchTracking(MotionEvent ev) { TraceHelper.INSTANCE.beginSection(UP_EVT); + if (DEBUG) { + Log.d(TAG, "finishTouchTracking: mPassedWindowMoveSlop=" + mPassedWindowMoveSlop); + Log.d(TAG, "finishTouchTracking: mInteractionHandler=" + mInteractionHandler); + Log.d(TAG, "finishTouchTracking: ev=" + ev); + } boolean isCanceled = ev.getActionMasked() == ACTION_CANCEL; if (mPassedWindowMoveSlop && mInteractionHandler != null) { @@ -438,13 +489,21 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC : velocityYPxPerMs; mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement); mInteractionHandler.onGestureEnded(velocityPxPerMs, - new PointF(velocityXPxPerMs, velocityYPxPerMs)); + new PointF(velocityXPxPerMs, velocityYPxPerMs), + Math.abs(mDownPos.x - mLastPos.x) > mTouchSlop); } } else { // Since we start touch tracking on DOWN, we may reach this state without actually // starting the gesture. In that case, we need to clean-up an unfinished or un-started // animation. + if (DEBUG) { + Log.d(TAG, "finishTouchTracking: mActiveCallbacks=" + mActiveCallbacks); + } if (mActiveCallbacks != null && mInteractionHandler != null) { + if (DEBUG) { + Log.d(TAG, "finishTouchTracking: isRecentsAnimationRunning=" + + mTaskAnimationManager.isRecentsAnimationRunning()); + } if (mTaskAnimationManager.isRecentsAnimationRunning()) { // The animation started, but with no movement, in this case, there will be no // animateToProgress so we have to manually finish here. In the case of @@ -470,6 +529,8 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC mVelocityTracker = null; } mMotionPauseDetector.clear(); + // Clear ref to recents view and launcher activity on action up or cancel to avoid leak + mRecentsViewDispatcher.clearConsumer(); } @Override @@ -533,9 +594,16 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC private static class FinishImmediatelyHandler implements RecentsAnimationCallbacks.RecentsAnimationListener { + @Override public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { + RecentsAnimationTargets targets, TransitionInfo transitionInfo) { + if (DEBUG) { + Log.d(TAG, "FinishImmediatelyHandler: queuing callback"); + } Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> { + if (DEBUG) { + Log.d(TAG, "FinishImmediatelyHandler: running callback"); + } controller.finish(false /* toRecents */, null); }); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java index c61f71d330..34ac98361b 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.Utilities; import com.android.launcher3.statemanager.BaseState; +import com.android.launcher3.statemanager.StatefulContainer; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.views.BaseDragLayer; @@ -41,7 +42,8 @@ import com.android.systemui.shared.system.InputMonitorCompat; /** * Input consumer for handling touch on the recents/Launcher activity. */ -public class OverviewInputConsumer, T extends RecentsViewContainer> +public class OverviewInputConsumer, + T extends RecentsViewContainer & StatefulContainer> implements InputConsumer { private final T mContainer; @@ -53,12 +55,15 @@ public class OverviewInputConsumer, T extends RecentsView private final int[] mLocationOnScreen = new int[2]; private final boolean mStartingInActivityBounds; + private boolean mTargetHandledTouch; - private boolean mHasSetTouchModeForFirstDPadEvent; private boolean mIsWaitingForAttachToWindow; - public OverviewInputConsumer(GestureState gestureState, T container, - @Nullable InputMonitorCompat inputMonitor, boolean startingInActivityBounds) { + public OverviewInputConsumer( + GestureState gestureState, + T container, + @Nullable InputMonitorCompat inputMonitor, + boolean startingInActivityBounds) { mContainer = container; mInputMonitor = inputMonitor; mStartingInActivityBounds = startingInActivityBounds; @@ -74,6 +79,11 @@ public class OverviewInputConsumer, T extends RecentsView return TYPE_OVERVIEW; } + @Override + public int getDisplayId() { + return mGestureState.getDisplayId(); + } + @Override public boolean allowInterceptByParent() { return !mTargetHandledTouch; @@ -104,9 +114,6 @@ public class OverviewInputConsumer, T extends RecentsView mInputMonitor.pilferPointers(); } } - if (mHasSetTouchModeForFirstDPadEvent) { - mContainer.getRootView().clearFocus(); - } } @Override @@ -127,11 +134,14 @@ public class OverviewInputConsumer, T extends RecentsView break; case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: - if (mHasSetTouchModeForFirstDPadEvent) { - break; - } + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_TAB: View viewRoot = mContainer.getRootView(); if (viewRoot.isAttachedToWindow()) { + if (!viewRoot.getViewRootImpl().getView().isInTouchMode()) { + break; + } setTouchModeChanged(viewRoot); break; } @@ -166,7 +176,6 @@ public class OverviewInputConsumer, T extends RecentsView // to focused views, while focus can only be requested in // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To // note, here we launch overview with live tile. - mHasSetTouchModeForFirstDPadEvent = true; viewRoot.getViewRootImpl().touchModeChanged(false); } } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java index 42e8694f6a..7838e8608e 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java @@ -24,11 +24,10 @@ import android.content.Context; import android.graphics.PointF; import android.view.MotionEvent; -import com.android.launcher3.BaseActivity; -import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.views.ActivityContext; import com.android.quickstep.GestureState; import com.android.quickstep.InputConsumer; import com.android.quickstep.RecentsAnimationDeviceState; @@ -44,9 +43,12 @@ public class OverviewWithoutFocusInputConsumer implements InputConsumer, private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker; private final GestureState mGestureState; - public OverviewWithoutFocusInputConsumer(Context context, - RecentsAnimationDeviceState deviceState, GestureState gestureState, - InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) { + public OverviewWithoutFocusInputConsumer( + Context context, + RecentsAnimationDeviceState deviceState, + GestureState gestureState, + InputMonitorCompat inputMonitor, + boolean disableHorizontalSwipe) { mContext = context; mGestureState = gestureState; mInputMonitor = inputMonitor; @@ -59,6 +61,11 @@ public class OverviewWithoutFocusInputConsumer implements InputConsumer, return TYPE_OVERVIEW_WITHOUT_FOCUS; } + @Override + public int getDisplayId() { + return mGestureState.getDisplayId(); + } + @Override public boolean allowInterceptByParent() { return !mTriggerSwipeUpTracker.interceptedTouch(); @@ -80,7 +87,7 @@ public class OverviewWithoutFocusInputConsumer implements InputConsumer, @Override public void onSwipeUp(boolean wasFling, PointF finalVelocity) { startHomeIntentSafely(mContext, mGestureState.getHomeIntent(), null, TAG); - BaseActivity activity = BaseDraggingActivity.fromContext(mContext); + ActivityContext activity = ActivityContext.lookupContext(mContext); int state = (mGestureState != null && mGestureState.getEndTarget() != null) ? mGestureState.getEndTarget().containerType : LAUNCHER_STATE_HOME; diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java index 6dcb7bceff..182841278e 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.graphics.Point; import android.view.MotionEvent; +import android.window.TransitionInfo; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; @@ -88,9 +89,12 @@ public class ProgressDelegateInputConsumer implements InputConsumer, private RecentsAnimationController mRecentsAnimationController; private Boolean mFlingEndsOnHome; - public ProgressDelegateInputConsumer(Context context, - TaskAnimationManager taskAnimationManager, GestureState gestureState, - InputMonitorCompat inputMonitorCompat, AnimatedFloat progress) { + public ProgressDelegateInputConsumer( + Context context, + TaskAnimationManager taskAnimationManager, + GestureState gestureState, + InputMonitorCompat inputMonitorCompat, + AnimatedFloat progress) { mContext = context; mTaskAnimationManager = taskAnimationManager; mGestureState = gestureState; @@ -116,6 +120,11 @@ public class ProgressDelegateInputConsumer implements InputConsumer, return TYPE_PROGRESS_DELEGATE; } + @Override + public int getDisplayId() { + return mGestureState.getDisplayId(); + } + @Override public void onMotionEvent(MotionEvent ev) { if (mFlingEndsOnHome == null) { @@ -166,13 +175,13 @@ public class ProgressDelegateInputConsumer implements InputConsumer, mRecentsAnimationController.finishController(endToRecents /* toRecents */, null /* callback */, false /* sendUserLeaveHint */); } else if (endToRecents) { - startHomeIntentSafely(mContext, null, TAG); + startHomeIntentSafely(mContext, null, TAG, getDisplayId()); } } @Override public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { + RecentsAnimationTargets targets, TransitionInfo transitionInfo) { mRecentsAnimationController = controller; mStateCallback.setState(STATE_TARGET_RECEIVED); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java deleted file mode 100644 index 349f4d2f2f..0000000000 --- a/quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.inputconsumers; - -import android.view.MotionEvent; - -import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.quickstep.InputConsumer; -import com.android.quickstep.TaskAnimationManager; - -import java.util.function.Supplier; - -/** - * A NO_OP input consumer which also resets any pending gesture - */ -public class ResetGestureInputConsumer implements InputConsumer { - - private final TaskAnimationManager mTaskAnimationManager; - private final Supplier mActivityContextSupplier; - - public ResetGestureInputConsumer( - TaskAnimationManager taskAnimationManager, - Supplier activityContextSupplier) { - mTaskAnimationManager = taskAnimationManager; - mActivityContextSupplier = activityContextSupplier; - } - - @Override - public int getType() { - return TYPE_RESET_GESTURE; - } - - @Override - public void onMotionEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN - && mTaskAnimationManager.isRecentsAnimationRunning()) { - TaskbarActivityContext tac = mActivityContextSupplier.get(); - mTaskAnimationManager.finishRunningRecentsAnimation( - /* toHome= */ tac != null && !tac.isInApp()); - } - } -} diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.kt b/quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.kt new file mode 100644 index 0000000000..96e7943061 --- /dev/null +++ b/quickstep/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.inputconsumers + +import android.view.MotionEvent +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.quickstep.InputConsumer +import com.android.quickstep.TaskAnimationManager +import java.util.function.Supplier + +/** A NO_OP input consumer which also resets any pending gesture */ +class ResetGestureInputConsumer( + private val displayId: Int, + private val taskAnimationManager: TaskAnimationManager, + private val activityContextSupplier: Supplier, +) : InputConsumer { + override fun getType() = InputConsumer.TYPE_RESET_GESTURE + + override fun getDisplayId() = displayId + + override fun onMotionEvent(ev: MotionEvent) { + if ( + ev.action == MotionEvent.ACTION_DOWN && taskAnimationManager.isRecentsAnimationRunning + ) { + val tac = activityContextSupplier.get() + taskAnimationManager.finishRunningRecentsAnimation( + /* toHome= */ tac != null && !tac.isInApp + ) + } + } +} diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java index d73c23ff51..9dc27de5f6 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/ScreenPinnedInputConsumer.java @@ -36,9 +36,12 @@ public class ScreenPinnedInputConsumer implements InputConsumer { private final float mMotionPauseMinDisplacement; private final MotionPauseDetector mMotionPauseDetector; + private final int mDisplayId; + private float mTouchDownY; public ScreenPinnedInputConsumer(Context context, GestureState gestureState) { + mDisplayId = gestureState.getDisplayId(); mMotionPauseMinDisplacement = context.getResources().getDimension( R.dimen.motion_pause_detector_min_displacement_from_app); mMotionPauseDetector = new MotionPauseDetector(context, true /* makePauseHarderToTrigger*/); @@ -60,6 +63,11 @@ public class ScreenPinnedInputConsumer implements InputConsumer { return TYPE_SCREEN_PINNED; } + @Override + public int getDisplayId() { + return mDisplayId; + } + @Override public void onMotionEvent(MotionEvent ev) { float y = ev.getY(); diff --git a/quickstep/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java index 871d0759c5..ad1a01b74e 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/SysUiOverlayInputConsumer.java @@ -47,11 +47,15 @@ public class SysUiOverlayInputConsumer implements InputConsumer, private final InputMonitorCompat mInputMonitor; private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker; + private final int mDisplayId; + public SysUiOverlayInputConsumer( Context context, + int displayId, RecentsAnimationDeviceState deviceState, InputMonitorCompat inputMonitor) { mContext = context; + mDisplayId = displayId; mInputMonitor = inputMonitor; mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, true, deviceState.getNavBarPosition(), this); @@ -62,6 +66,11 @@ public class SysUiOverlayInputConsumer implements InputConsumer, return TYPE_SYSUI_OVERLAY; } + @Override + public int getDisplayId() { + return mDisplayId; + } + @Override public boolean allowInterceptByParent() { return !mTriggerSwipeUpTracker.interceptedTouch(); diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java index 95295b0eb9..5504239741 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java @@ -15,24 +15,28 @@ */ package com.android.quickstep.inputconsumers; -import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_MOVE; -import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.INVALID_POINTER_ID; +import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; +import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; import static com.android.launcher3.Flags.enableCursorHoverStates; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent; import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING; +import static com.android.systemui.shared.Flags.cursorHotCorner; import android.content.Context; import android.content.res.Resources; import android.graphics.PointF; import android.graphics.Rect; +import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.Looper; +import android.view.Display; import android.view.InputDevice; import android.view.MotionEvent; +import android.view.RoundedCorner; import android.view.VelocityTracker; import android.view.ViewConfiguration; @@ -43,12 +47,12 @@ import com.android.launcher3.R; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarThresholdUtils; import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback; -import com.android.launcher3.taskbar.bubbles.BubbleControllers; import com.android.launcher3.touch.OverScroll; import com.android.launcher3.util.DisplayController; import com.android.quickstep.GestureState; import com.android.quickstep.InputConsumer; import com.android.quickstep.OverviewCommandHelper; +import com.android.quickstep.OverviewCommandHelper.CommandType; import com.android.systemui.shared.system.InputMonitorCompat; /** @@ -64,14 +68,13 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { private final TaskbarActivityContext mTaskbarActivityContext; private final OverviewCommandHelper mOverviewCommandHelper; + private final DisplayManager mDisplayManager; private final float mUnstashArea; + private final int mActionCornerPadding; private final int mTaskbarNavThreshold; private final int mTaskbarNavThresholdY; private final boolean mIsTaskbarAllAppsOpen; private boolean mHasPassedTaskbarNavThreshold; - private boolean mIsInBubbleBarArea; - private boolean mIsVerticalGestureOverBubbleBar; - private boolean mIsPassedBubbleBarSlop; private final int mTouchSlop; private final PointF mDownPos = new PointF(); @@ -94,23 +97,36 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { // Velocity defined as dp per s private float mTaskbarSlowVelocityYThreshold; - public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate, - InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext, - OverviewCommandHelper overviewCommandHelper, GestureState gestureState) { - super(delegate, inputMonitor); + public TaskbarUnstashInputConsumer( + Context context, + InputConsumer delegate, + InputMonitorCompat inputMonitor, + TaskbarActivityContext taskbarActivityContext, + OverviewCommandHelper overviewCommandHelper, + GestureState gestureState) { + super(gestureState.getDisplayId(), delegate, inputMonitor); mTaskbarActivityContext = taskbarActivityContext; + mIsTransientTaskbar = DisplayController.isTransientTaskbar(context); mOverviewCommandHelper = overviewCommandHelper; + mDisplayManager = context.getSystemService(DisplayManager.class); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); Resources res = context.getResources(); mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area); - mTaskbarNavThreshold = TaskbarThresholdUtils.getFromNavThreshold(res, - taskbarActivityContext.getDeviceProfile()); - mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx + mActionCornerPadding = res.getDimensionPixelSize( + R.dimen.transient_taskbar_action_corner_padding); + + boolean pinnedTaskbarWithAutoStashing = + mTaskbarActivityContext.shouldAllowTaskbarToAutoStash() && !mIsTransientTaskbar; + + mTaskbarNavThreshold = + pinnedTaskbarWithAutoStashing ? 0 : TaskbarThresholdUtils.getFromNavThreshold(res, + taskbarActivityContext.getDeviceProfile()); + + mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().getDeviceProperties().getHeightPx() - mTaskbarNavThreshold; mIsTaskbarAllAppsOpen = mTaskbarActivityContext.isTaskbarAllAppsOpen(); - mIsTransientTaskbar = DisplayController.isTransientTaskbar(context); mTaskbarSlowVelocityYThreshold = res.getDimensionPixelSize(R.dimen.taskbar_slow_velocity_y_threshold); @@ -159,9 +175,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) { mTransitionCallback.onActionDown(); } - if (mIsTransientTaskbar && isInBubbleBarArea(x)) { - mIsInBubbleBarArea = true; - } break; case MotionEvent.ACTION_POINTER_UP: int ptrIdx = ev.getActionIndex(); @@ -185,32 +198,16 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { float dX = mLastPos.x - mDownPos.x; float dY = mLastPos.y - mDownPos.y; - if (!mIsPassedBubbleBarSlop && mIsInBubbleBarArea) { - boolean passedSlop = - Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop; - if (passedSlop) { - mIsPassedBubbleBarSlop = true; - mIsVerticalGestureOverBubbleBar = Math.abs(dY) > Math.abs(dX); - if (mIsVerticalGestureOverBubbleBar) { - setActive(ev); - } - } - } - - if (mIsTransientTaskbar) { + if (mTaskbarActivityContext.shouldAllowTaskbarToAutoStash()) { boolean passedTaskbarNavThreshold = dY < 0 && Math.abs(dY) >= mTaskbarNavThreshold; + // we only care about nav thresholds when we are transient taskbar if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold && !mGestureState.isInExtendedSlopRegion()) { mHasPassedTaskbarNavThreshold = true; - if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) { - mTaskbarActivityContext.onSwipeToOpenBubblebar(); - } else { - mTaskbarActivityContext.onSwipeToUnstashTaskbar(); - } + mTaskbarActivityContext.onSwipeToUnstashTaskbar(true); } - if (dY < 0) { dY = -OverScroll.dampedScroll(-dY, mTaskbarNavThresholdY); if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) { @@ -225,46 +222,13 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { break; case MotionEvent.ACTION_BUTTON_RELEASE: if (isStashedTaskbarHovered) { - mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME); + mOverviewCommandHelper.addCommand(CommandType.HOME); } break; } } - boolean isMovingInBubbleBarArea = mIsInBubbleBarArea && ev.getAction() == ACTION_MOVE; if (!isStashedTaskbarHovered) { - // if we're moving in the bubble bar area but we haven't passed the slop yet, don't - // propagate to the delegate, until we can determine the direction of the gesture. - if (!isMovingInBubbleBarArea || mIsPassedBubbleBarSlop) { - mDelegate.onMotionEvent(ev); - } - } - } else if (mIsVerticalGestureOverBubbleBar) { - // if we get here then this gesture is a vertical swipe over the bubble bar. - // we're also active and there's no need to delegate any additional motion events. the - // rest of the gesture will be handled here. - switch (ev.getAction()) { - case ACTION_MOVE: - int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex == INVALID_POINTER_ID) { - break; - } - mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); - - float dY = mLastPos.y - mDownPos.y; - - // bubble bar swipe gesture uses the same threshold as the taskbar. - boolean passedTaskbarNavThreshold = dY < 0 - && Math.abs(dY) >= mTaskbarNavThreshold; - - if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) { - mHasPassedTaskbarNavThreshold = true; - mTaskbarActivityContext.onSwipeToOpenBubblebar(); - } - break; - case ACTION_UP: - case ACTION_CANCEL: - cleanupAfterMotionEvent(); - break; + mDelegate.onMotionEvent(ev); } } } @@ -285,10 +249,13 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { } float velocityYPxPerS = mVelocityTracker.getYVelocity(); + float dY = Math.abs(mLastPos.y - mDownPos.y); if (mCanPlayTaskbarBgAlphaAnimation && mMotionMoveCount >= NUM_MOTION_MOVE_THRESHOLD // Arbitrary value && velocityYPxPerS != 0 // Ignore these - && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold) { + && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold + && dY != 0 + && dY > mTouchSlop) { mTaskbarActivityContext.playTaskbarBackgroundAlphaAnimation(); mCanPlayTaskbarBgAlphaAnimation = false; } @@ -301,9 +268,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { mTransitionCallback.onActionEnd(); } mHasPassedTaskbarNavThreshold = false; - mIsInBubbleBarArea = false; - mIsVerticalGestureOverBubbleBar = false; - mIsPassedBubbleBarSlop = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); @@ -313,22 +277,6 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { mMotionMoveCount = 0; } - private boolean isInBubbleBarArea(float x) { - if (mTaskbarActivityContext == null || !mIsTransientTaskbar) { - return false; - } - BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers(); - if (controllers == null) { - return false; - } - if (controllers.bubbleStashController.isStashed()) { - return controllers.bubbleStashedHandleViewController.containsX((int) x); - } else { - Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds(); - return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right; - } - } - /** * Listen for hover events for the stashed taskbar. * @@ -347,23 +295,23 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { if (mIsStashedTaskbarHovered) { updateHoveredTaskbarState((int) ev.getX(), (int) ev.getY()); } else { - updateUnhoveredTaskbarState((int) ev.getX(), (int) ev.getY()); + updateUnhoveredTaskbarState((int) ev.getX(), (int) ev.getY(), ev.getDisplayId()); } } private void updateHoveredTaskbarState(int x, int y) { DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile(); mBottomEdgeBounds.set( - (dp.widthPx - (int) mUnstashArea) / 2, - dp.heightPx - mStashedTaskbarBottomEdge, - (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea), - dp.heightPx); + (dp.getDeviceProperties().getWidthPx() - (int) mUnstashArea) / 2, + dp.getDeviceProperties().getHeightPx() - mStashedTaskbarBottomEdge, + (int) (((dp.getDeviceProperties().getWidthPx() - mUnstashArea) / 2) + mUnstashArea), + dp.getDeviceProperties().getHeightPx()); if (mBottomEdgeBounds.contains(x, y)) { // start a single unstash timeout if hovering bottom edge under the hinted taskbar. if (!sUnstashHandler.hasMessagesOrCallbacks()) { sUnstashHandler.postDelayed(() -> { - mTaskbarActivityContext.onSwipeToUnstashTaskbar(); + mTaskbarActivityContext.onSwipeToUnstashTaskbar(false); mIsStashedTaskbarHovered = false; }, HOVER_TASKBAR_UNSTASH_TIMEOUT); } @@ -376,22 +324,37 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { } } - private void updateUnhoveredTaskbarState(int x, int y) { + private void updateUnhoveredTaskbarState(int x, int y, int displayId) { sUnstashHandler.removeCallbacksAndMessages(null); DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile(); mBottomEdgeBounds.set( 0, - dp.heightPx - mBottomScreenEdge, - dp.widthPx, - dp.heightPx); + dp.getDeviceProperties().getHeightPx() - mBottomScreenEdge, + dp.getDeviceProperties().getWidthPx(), + dp.getDeviceProperties().getHeightPx()); + + if (cursorHotCorner() && mDisplayManager != null) { + Display display = mDisplayManager.getDisplay(displayId); + if (display != null) { + RoundedCorner leftBottomCorner = display.getRoundedCorner(POSITION_BOTTOM_LEFT); + int leftCornerRadius = + leftBottomCorner == null ? 0 : leftBottomCorner.getRadius(); + RoundedCorner rightBottomCorner = display.getRoundedCorner( + POSITION_BOTTOM_RIGHT); + int rightCornerRadius = + rightBottomCorner == null ? 0 : rightBottomCorner.getRadius(); + mBottomEdgeBounds.inset(leftCornerRadius + mActionCornerPadding, 0, + rightCornerRadius + mActionCornerPadding, 0); + } + } if (isStashedTaskbarHovered(x, y)) { // If enter hovering stashed taskbar, start hint. startStashedTaskbarHover(/* isHovered = */ true); } else if (mBottomEdgeBounds.contains(x, y)) { // If hover screen's bottom edge not below the stashed taskbar, unstash it. - mTaskbarActivityContext.onSwipeToUnstashTaskbar(); + mTaskbarActivityContext.onSwipeToUnstashTaskbar(false); } } @@ -408,10 +371,11 @@ public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { } DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile(); mStashedTaskbarHandleBounds.set( - (dp.widthPx - (int) mUnstashArea) / 2, - dp.heightPx - dp.stashedTaskbarHeight, - (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea), - dp.heightPx); + (dp.getDeviceProperties().getWidthPx() - (int) mUnstashArea) / 2, + dp.getDeviceProperties().getHeightPx() + - dp.getTaskbarProfile().getStashedTaskbarHeight(), + (int) (((dp.getDeviceProperties().getWidthPx() - mUnstashArea) / 2) + mUnstashArea), + dp.getDeviceProperties().getHeightPx()); return mStashedTaskbarHandleBounds.contains(x, y); } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java index f3e21e1399..a53a39524b 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java @@ -35,9 +35,12 @@ public class TrackpadStatusBarInputConsumer extends DelegateInputConsumer { private final PointF mDown = new PointF(); private boolean mHasPassedTouchSlop; - public TrackpadStatusBarInputConsumer(Context context, InputConsumer delegate, + public TrackpadStatusBarInputConsumer( + Context context, + int displayId, + InputConsumer delegate, InputMonitorCompat inputMonitor) { - super(delegate, inputMonitor); + super(displayId, delegate, inputMonitor); mSystemUiProxy = SystemUiProxy.INSTANCE.get(context); mTouchSlop = 2 * ViewConfiguration.get(context).getScaledTouchSlop(); diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java index 5b3afbb479..6794e8b312 100644 --- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java +++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java @@ -18,6 +18,7 @@ package com.android.quickstep.interaction; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; +import static com.android.app.animation.Interpolators.ACCELERATE; import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.Utilities.mapBoundToRange; @@ -29,7 +30,6 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; @@ -42,8 +42,10 @@ import android.graphics.PointF; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.Shader.TileMode; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.SystemProperties; import android.os.VibrationEffect; import android.os.Vibrator; import android.text.TextUtils; @@ -62,16 +64,22 @@ import androidx.core.graphics.ColorUtils; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; +import com.android.launcher3.RemoveAnimationSettingsTracker; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.taskbar.TaskbarManager; import com.android.launcher3.util.Executors; import com.android.quickstep.GestureState; +import com.android.quickstep.OverviewComponentObserver; +import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener; import com.android.quickstep.TouchInteractionService.TISBinder; +import com.android.quickstep.util.ActivityPreloadUtil; import com.android.quickstep.util.LottieAnimationColorUtils; import com.android.quickstep.util.TISBindHelper; +import com.android.wm.shell.shared.TypefaceUtils.FontFamily; import com.airbnb.lottie.LottieAnimationView; @@ -79,15 +87,17 @@ import java.net.URISyntaxException; import java.util.Map; /** - * A page shows after SUW flow to hint users to swipe up from the bottom of the - * screen to go home + * A page shows after SUW flow to hint users to swipe up from the bottom of the screen to go home * for the gestural system navigation. */ public class AllSetActivity extends Activity { private static final String TAG = "AllSetActivity"; private static final String LOG_TAG = "AllSetActivity"; - private static final String URI_SYSTEM_NAVIGATION_SETTING = "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S.:settings:fragment_args_key=gesture_system_navigation_input_summary;S.:settings:show_fragment=com.android.settings.gestures.SystemNavigationGestureSettings;end"; + private static final String URI_SYSTEM_NAVIGATION_SETTING = + "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S.:settings:fragment_args_key=gesture_system_navigation_input_summary;S.:settings:show_fragment=com.android.settings.gestures.SystemNavigationGestureSettings;end"; + private static final String INTENT_ACTION_ACTIVITY_CLOSED = + "com.android.quickstep.interaction.ACTION_ALL_SET_ACTIVITY_CLOSED"; private static final String EXTRA_ACCENT_COLOR_DARK_MODE = "suwColorAccentDark"; private static final String EXTRA_ACCENT_COLOR_LIGHT_MODE = "suwColorAccentLight"; private static final String EXTRA_DEVICE_NAME = "suwDeviceName"; @@ -95,97 +105,139 @@ public class AllSetActivity extends Activity { private static final String LOTTIE_PRIMARY_COLOR_TOKEN = ".primary"; private static final String LOTTIE_TERTIARY_COLOR_TOKEN = ".tertiary"; + private static final String SUW_THEME_SYSTEM_PROPERTY = "setupwizard.theme"; + private static final String GLIF_EXPRESSIVE_THEME = "glif_expressive"; + private static final String GLIF_EXPRESSIVE_LIGHT_THEME = "glif_expressive_light"; + + private boolean mIsExpressiveThemeEnabledInSUW = false; + private static final float HINT_BOTTOM_FACTOR = 1 - .94f; private static final int MAX_SWIPE_DURATION = 350; + private static final int WALLPAPER_BLUR_RADIUS = 30; + private static final float ANIMATION_PAUSE_ALPHA_THRESHOLD = 0.1f; + private static final String KEY_BACKGROUND_ANIMATION_TOGGLED_ON = + "background_animation_toggled_on"; + private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate); + private final InvariantDeviceProfile.OnIDPChangeListener mOnIDPChangeListener = + modelPropertiesChanged -> updateHint(); + private TISBindHelper mTISBindHelper; private BgDrawable mBackground; private View mRootView; private float mSwipeUpShift; - @Nullable - private Vibrator mVibrator; + @Nullable private Vibrator mVibrator; private LottieAnimationView mAnimatedBackground; private Animator.AnimatorListener mBackgroundAnimatorListener; private AnimatorPlaybackController mLauncherStartAnim = null; + // Auto play background animation by default + private boolean mBackgroundAnimationToggledOn = true; + + private TextView mHintView; + + private final OverviewChangeListener mOverviewChangeListener = this::onOverviewTargetChange; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + String SUWTheme = SystemProperties.get(SUW_THEME_SYSTEM_PROPERTY, ""); + mIsExpressiveThemeEnabledInSUW = SUWTheme.equals(GLIF_EXPRESSIVE_THEME) || SUWTheme.equals( + GLIF_EXPRESSIVE_LIGHT_THEME); + if (mIsExpressiveThemeEnabledInSUW) setTheme(R.style.AllSetTheme_Expressive); + super.onCreate(savedInstanceState); - setContentView(R.layout.activity_allset); - mRootView = findViewById(R.id.root_view); - mRootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + boolean isDarkTheme = + (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + String suwDeviceName = getIntent().getStringExtra(EXTRA_DEVICE_NAME); + if (mIsExpressiveThemeEnabledInSUW) { + setupExpressiveTheme(suwDeviceName); + } else { + setupDefaultTheme(savedInstanceState, isDarkTheme, suwDeviceName); + } + initializeCommonViewsAndListeners(); + configureSystemUI(isDarkTheme); - Resources resources = getResources(); - int mode = resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - boolean isDarkTheme = mode == Configuration.UI_MODE_NIGHT_YES; + mTISBindHelper = new TISBindHelper(this, this::onTISConnected); + mVibrator = getSystemService(Vibrator.class); + getIDP().addOnChangeListener(mOnIDPChangeListener); + OverviewComponentObserver.INSTANCE.get(this) + .addOverviewChangeListener(mOverviewChangeListener); + ActivityPreloadUtil.preloadOverviewForSUWAllSet(this); + } + private void configureSystemUI(boolean isDarkTheme) { int systemBarsMask = APPEARANCE_LIGHT_STATUS_BARS | APPEARANCE_LIGHT_NAVIGATION_BARS; int systemBarsAppearance = isDarkTheme ? 0 : systemBarsMask; Window window = getWindow(); WindowInsetsController insetsController = window == null ? null : window.getInsetsController(); + if (insetsController != null) { insetsController.setSystemBarsAppearance(systemBarsAppearance, systemBarsMask); } + if (mIsExpressiveThemeEnabledInSUW && window != null) { + window.setBackgroundBlurRadius(WALLPAPER_BLUR_RADIUS); + } + mRootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } - Intent intent = getIntent(); - int accentColor = intent.getIntExtra( - isDarkTheme ? EXTRA_ACCENT_COLOR_DARK_MODE : EXTRA_ACCENT_COLOR_LIGHT_MODE, - isDarkTheme ? Color.WHITE : Color.BLACK); + private void initializeCommonViewsAndListeners() { + mHintView = findViewById(R.id.hint); + mHintView.setAccessibilityDelegate(new SkipButtonAccessibilityDelegate()); + updateHint(); - ((ImageView) findViewById(R.id.icon)).getDrawable().mutate().setTint(accentColor); + mSwipeUpShift = getResources().getDimension(R.dimen.allset_swipe_up_shift); - mBackground = new BgDrawable(this); - mRootView.setBackground(mBackground); - mSwipeUpShift = resources.getDimension(R.dimen.allset_swipe_up_shift); - - TextView subtitle = findViewById(R.id.subtitle); - String suwDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME); - subtitle.setText(getString( - R.string.allset_description_generic, - !TextUtils.isEmpty(suwDeviceName) - ? suwDeviceName - : getString(R.string.default_device_name))); - - TextView settings = findViewById(R.id.navigation_settings); - settings.setTextColor(accentColor); - settings.setOnClickListener(v -> { + View navigationSettings = findViewById(R.id.navigation_settings); + navigationSettings.setOnClickListener(v -> { try { + // This is the action that starts the system navigation settings page startActivityForResult( Intent.parseUri(URI_SYSTEM_NAVIGATION_SETTING, 0), 0); } catch (URISyntaxException e) { Log.e(LOG_TAG, "Failed to parse system nav settings intent", e); } }); + } - TextView hint = findViewById(R.id.hint); - DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this); - if (!dp.isGestureMode) { - hint.setText(R.string.allset_button_hint); - } - hint.setAccessibilityDelegate(new SkipButtonAccessibilityDelegate()); + private void setupDefaultTheme(@Nullable Bundle savedInstanceState, boolean isDarkTheme, + String suwDeviceName) { + setContentView(R.layout.activity_allset); + mRootView = findViewById(R.id.root_view); - mTISBindHelper = new TISBindHelper(this, this::onTISConnected); + mBackground = new BgDrawable(this); + mRootView.setBackground(mBackground); + + int accentColor = getIntent().getIntExtra( + isDarkTheme ? EXTRA_ACCENT_COLOR_DARK_MODE : EXTRA_ACCENT_COLOR_LIGHT_MODE, + isDarkTheme ? Color.WHITE : Color.BLACK); + + ((ImageView) findViewById(R.id.icon)).getDrawable().mutate().setTint(accentColor); + TextView navigationSettings = findViewById(R.id.navigation_settings); + navigationSettings.setTextColor(accentColor); + + TextView subtitle = findViewById(R.id.subtitle); + subtitle.setText(TextUtils.isEmpty(suwDeviceName) + ? getString(R.string.allset_description_fallback) + : getString(R.string.allset_description_generic, suwDeviceName)); - mVibrator = getSystemService(Vibrator.class); mAnimatedBackground = findViewById(R.id.animated_background); - // There's a bug in the currently used external Lottie library (v5.2.0), and it - // doesn't load - // the correct animation from the raw resources when configuration changes, so - // we need to + // There's a bug in the currently used external Lottie library (v5.2.0), and it doesn't load + // the correct animation from the raw resources when configuration changes, so we need to // manually load the resource and pass it to Lottie. - mAnimatedBackground.setAnimation(resources.openRawResource(R.raw.all_set_page_bg), + mAnimatedBackground.setAnimation(getResources().openRawResource(R.raw.all_set_page_bg), null); LottieAnimationColorUtils.updateToColorResources( @@ -194,7 +246,74 @@ public class AllSetActivity extends Activity { LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary), getTheme()); - startBackgroundAnimation(dp.isTablet); + mBackgroundAnimationToggledOn = savedInstanceState == null + || savedInstanceState.getBoolean(KEY_BACKGROUND_ANIMATION_TOGGLED_ON, true); + // The animated background is behind a scroll view, which intercepts all input. + // However, the content view also covers the full screen + requireViewById(R.id.content).setOnClickListener(v -> { + mBackgroundAnimationToggledOn = !mBackgroundAnimationToggledOn; + maybeResumeOrPauseBackgroundAnimation(); + }); + setUpBackgroundAnimation(getDP().getDeviceProperties().isTablet()); + } + + private void setupExpressiveTheme(String suwDeviceName) { + setContentView(R.layout.activity_allset_expressive); + mRootView = findViewById(R.id.root_view); + + TextView title = findViewById(R.id.title); + TextView subtitle = findViewById(R.id.subtitle); + mHintView = findViewById(R.id.hint); + TextView navigationSettings = findViewById(R.id.navigation_settings); + title.setText(TextUtils.isEmpty(suwDeviceName) + ? getString(R.string.allset_title_expressive_fallback) + : getString(R.string.allset_title_expressive, suwDeviceName)); + String deviceType = getDP().getDeviceProperties().isTablet() + ? getString(R.string.allset_device_type_tablet) + : getString(R.string.allset_device_type_phone); + subtitle.setText(getString(R.string.allset_subtitle_expressive, deviceType)); + + title.setTypeface( + Typeface.create(FontFamily.GSF_HEADLINE_LARGE_EMPHASIZED.getValue(), + Typeface.NORMAL)); + subtitle.setTypeface( + Typeface.create(FontFamily.GSF_BODY_MEDIUM.getValue(), Typeface.NORMAL)); + mHintView.setTypeface( + Typeface.create(FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED.getValue(), + Typeface.NORMAL)); + navigationSettings.setTypeface( + Typeface.create(FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED.getValue(), + Typeface.NORMAL)); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (!mIsExpressiveThemeEnabledInSUW) { + outState.putBoolean(KEY_BACKGROUND_ANIMATION_TOGGLED_ON, mBackgroundAnimationToggledOn); + } + } + + private InvariantDeviceProfile getIDP() { + return LauncherAppState.getInstance(this).getInvariantDeviceProfile(); + } + + private DeviceProfile getDP() { + return getIDP().getDeviceProfile(this); + } + + private void updateHint() { + if (mIsExpressiveThemeEnabledInSUW) { + mHintView.setText( + getDP().getDeviceProperties().isGestureMode() + ? R.string.allset_hint_expressive + : R.string.allset_button_hint_expressive); + } else { + mHintView.setText( + getDP().getDeviceProperties().isGestureMode() + ? R.string.allset_hint + : R.string.allset_button_hint); + } } private void runOnUiHelperThread(Runnable runnable) { @@ -205,8 +324,8 @@ public class AllSetActivity extends Activity { Executors.UI_HELPER_EXECUTOR.execute(runnable); } - private void startBackgroundAnimation(boolean forTablet) { - if (!Utilities.ATLEAST_S || mVibrator == null) { + private void setUpBackgroundAnimation(boolean forTablet) { + if (mVibrator == null || mIsExpressiveThemeEnabledInSUW) { return; } boolean supportsThud = mVibrator.areAllPrimitivesSupported( @@ -219,42 +338,41 @@ public class AllSetActivity extends Activity { if (mBackgroundAnimatorListener == null) { VibrationEffect vibrationEffect = VibrationEffect.startComposition() .addPrimitive(supportsThud - ? VibrationEffect.Composition.PRIMITIVE_THUD - : VibrationEffect.Composition.PRIMITIVE_TICK, + ? VibrationEffect.Composition.PRIMITIVE_THUD + : VibrationEffect.Composition.PRIMITIVE_TICK, /* scale= */ forTablet ? 1.0f : 0.3f, /* delay= */ 50) .compose(); - mBackgroundAnimatorListener = new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - runOnUiHelperThread(() -> mVibrator.vibrate(vibrationEffect)); - } + mBackgroundAnimatorListener = + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + runOnUiHelperThread(() -> mVibrator.vibrate(vibrationEffect)); + } - @Override - public void onAnimationRepeat(Animator animation) { - runOnUiHelperThread(() -> mVibrator.vibrate(vibrationEffect)); - } + @Override + public void onAnimationRepeat(Animator animation) { + runOnUiHelperThread(() -> mVibrator.vibrate(vibrationEffect)); + } - @Override - public void onAnimationEnd(Animator animation) { - runOnUiHelperThread(mVibrator::cancel); - } + @Override + public void onAnimationEnd(Animator animation) { + runOnUiHelperThread(mVibrator::cancel); + } - @Override - public void onAnimationCancel(Animator animation) { - runOnUiHelperThread(mVibrator::cancel); - } - }; + @Override + public void onAnimationCancel(Animator animation) { + runOnUiHelperThread(mVibrator::cancel); + } + }; } mAnimatedBackground.addAnimatorListener(mBackgroundAnimatorListener); - mAnimatedBackground.playAnimation(); } private void setSetupUIVisible(boolean visible) { TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager(); - if (taskbarManager == null) - return; + if (taskbarManager == null) return; taskbarManager.setSetupUIVisible(visible); } @@ -267,19 +385,24 @@ public class AllSetActivity extends Activity { setSetupUIVisible(true); binder.setSwipeUpProxy(this::createSwipeUpProxy); } + if (mIsExpressiveThemeEnabledInSUW) { + getWindow().setBackgroundBlurRadius(WALLPAPER_BLUR_RADIUS); + } } private void onTISConnected(TISBinder binder) { setSetupUIVisible(isResumed()); binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null); - binder.setOverviewTargetChangeListener(binder::preloadOverviewForSUWAllSet); - binder.preloadOverviewForSUWAllSet(); TaskbarManager taskbarManager = binder.getTaskbarManager(); if (taskbarManager != null) { mLauncherStartAnim = taskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION); } } + private void onOverviewTargetChange(boolean isHomeAndOverviewSame) { + ActivityPreloadUtil.preloadOverviewForSUWAllSet(this); + } + @Override protected void onPause() { super.onPause(); @@ -296,27 +419,26 @@ public class AllSetActivity extends Activity { if (binder != null) { setSetupUIVisible(false); binder.setSwipeUpProxy(null); - binder.setOverviewTargetChangeListener(null); } } /** - * Should be called when we have successfully reached Launcher, so we dispatch - * to animation - * listeners to ensure the state matches the visual animation that just - * occurred. - */ + * Should be called when we have successfully reached Launcher, so we dispatch to animation + * listeners to ensure the state matches the visual animation that just occurred. + */ private void dispatchLauncherAnimStartEnd() { if (mLauncherStartAnim != null) { mLauncherStartAnim.dispatchOnStart(); mLauncherStartAnim.dispatchOnEnd(); mLauncherStartAnim = null; } + sendBroadcast(new Intent(INTENT_ACTION_ACTIVITY_CLOSED)); } @Override protected void onDestroy() { super.onDestroy(); + getIDP().removeOnChangeListener(mOnIDPChangeListener); mTISBindHelper.onDestroy(); clearBinderOverride(); if (mBackgroundAnimatorListener != null) { @@ -325,6 +447,8 @@ public class AllSetActivity extends Activity { if (!isChangingConfigurations()) { dispatchLauncherAnimStartEnd(); } + OverviewComponentObserver.INSTANCE.get(this) + .removeOverviewChangeListener(mOverviewChangeListener); } private AnimatedFloat createSwipeUpProxy(GestureState state) { @@ -341,8 +465,14 @@ public class AllSetActivity extends Activity { } private void maybeResumeOrPauseBackgroundAnimation() { - boolean shouldPlayAnimation = getContentViewAlphaForSwipeProgress() > ANIMATION_PAUSE_ALPHA_THRESHOLD - && isResumed(); + if (mIsExpressiveThemeEnabledInSUW) { + return; + } + boolean shouldPlayAnimation = + !RemoveAnimationSettingsTracker.INSTANCE.get(this).isRemoveAnimationEnabled() + && getContentViewAlphaForSwipeProgress() > ANIMATION_PAUSE_ALPHA_THRESHOLD + && isResumed() + && mBackgroundAnimationToggledOn; if (mAnimatedBackground.isAnimating() && !shouldPlayAnimation) { mAnimatedBackground.pauseAnimation(); } else if (!mAnimatedBackground.isAnimating() && shouldPlayAnimation) { @@ -351,7 +481,13 @@ public class AllSetActivity extends Activity { } private void onSwipeProgressUpdate() { - mBackground.setProgress(mSwipeProgress.value); + if (!mIsExpressiveThemeEnabledInSUW) { + mBackground.setProgress(mSwipeProgress.value); + } else { + getWindow().setBackgroundBlurRadius((int) mapBoundToRange( + mSwipeProgress.value, 0, HINT_BOTTOM_FACTOR, WALLPAPER_BLUR_RADIUS, 0, + ACCELERATE)); + } float alpha = getContentViewAlphaForSwipeProgress(); mRootView.setAlpha(alpha); mRootView.setTranslationY((alpha - 1) * mSwipeUpShift); @@ -380,7 +516,7 @@ public class AllSetActivity extends Activity { @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (action == AccessibilityAction.ACTION_CLICK.getId()) { - startHomeIntentSafely(AllSetActivity.this, null, TAG); + startHomeIntentSafely(AllSetActivity.this, null, TAG, getDisplayId()); finish(); return true; } @@ -399,7 +535,8 @@ public class AllSetActivity extends Activity { private final Matrix mMatrix = new Matrix(); private final ColorMatrix mColorMatrix = new ColorMatrix(); - private final ColorMatrixColorFilter mColorFilter = new ColorMatrixColorFilter(mColorMatrix); + private final ColorMatrixColorFilter mColorFilter = + new ColorMatrixColorFilter(mColorMatrix); private final int mColor; private float mProgress = 0; @@ -407,8 +544,8 @@ public class AllSetActivity extends Activity { BgDrawable(Context context) { mColor = context.getColor(R.color.all_set_page_background); mMaskGrad = new RadialGradient(0, 0, 1, - new int[] { ColorUtils.setAlphaComponent(mColor, 0), mColor }, - new float[] { 0, 1 }, TileMode.CLAMP); + new int[] {ColorUtils.setAlphaComponent(mColor, 0), mColor}, + new float[]{0, 1}, TileMode.CLAMP); mPaint.setShader(mMaskGrad); mPaint.setColorFilter(mColorFilter); @@ -430,7 +567,7 @@ public class AllSetActivity extends Activity { float size = PointF.length(x, height); float radius = size * mapRange(progress, START_SIZE_FACTOR, END_SIZE_FACTOR); - float y = mapRange(progress, height + radius, height / 2); + float y = mapRange(progress, height + radius , height / 2); mMatrix.setTranslate(x, y); mMatrix.postScale(radius, radius, x, y); mMaskGrad.setLocalMatrix(mMatrix); @@ -456,11 +593,9 @@ public class AllSetActivity extends Activity { } @Override - public void setAlpha(int i) { - } + public void setAlpha(int i) { } @Override - public void setColorFilter(ColorFilter colorFilter) { - } + public void setColorFilter(ColorFilter colorFilter) { } } } diff --git a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java index 742b0fc7e8..7a86db37c7 100644 --- a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java +++ b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java @@ -15,8 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -175,11 +173,8 @@ public class AnimatedTaskView extends ConstraintLayout { void setFakeTaskViewFillColor(@ColorInt int colorResId) { mFullTaskView.setBackgroundColor(colorResId); - - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()){ - mTopTaskView.getBackground().setTint(colorResId); - mBottomTaskView.getBackground().setTint(colorResId); - } + mTopTaskView.getBackground().setTint(colorResId); + mBottomTaskView.getBackground().setTint(colorResId); } @Override diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java index 6669da5b0e..7fe4278d65 100644 --- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java @@ -15,7 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION; import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE; @@ -40,40 +39,29 @@ final class BackGestureTutorialController extends TutorialController { BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) { super(fragment, tutorialType); // Set the Lottie animation colors specifically for the Back gesture - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - LottieAnimationColorUtils.updateToArgbColors( - mAnimatedGestureDemonstration, - Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack, - ".surfaceBack", fragment.mRootView.mColorSurfaceBack, - ".secondaryBack", fragment.mRootView.mColorSecondaryBack)); + LottieAnimationColorUtils.updateToArgbColors( + mAnimatedGestureDemonstration, + Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack, + ".surfaceBack", fragment.mRootView.mColorSurfaceBack, + ".secondaryBack", fragment.mRootView.mColorSecondaryBack)); - LottieAnimationColorUtils.updateToArgbColors( - mCheckmarkAnimation, - Map.of(".checkmark", - Utilities.isDarkTheme(mContext) - ? fragment.mRootView.mColorOnSurfaceBack - : fragment.mRootView.mColorSecondaryBack, - ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack)); - } + LottieAnimationColorUtils.updateToArgbColors( + mCheckmarkAnimation, + Map.of(".checkmark", + Utilities.isDarkTheme(mContext) + ? fragment.mRootView.mColorOnSurfaceBack + : fragment.mRootView.mColorSecondaryBack, + ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack)); } @Override public int getIntroductionTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.back_gesture_tutorial_title - : R.string.back_gesture_intro_title; + return R.string.back_gesture_tutorial_title; } @Override public int getIntroductionSubtitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.back_gesture_tutorial_subtitle - : R.string.back_gesture_intro_subtitle; - } - - @Override - public int getSpokenIntroductionSubtitle() { - return R.string.back_gesture_spoken_intro_subtitle; + return R.string.back_gesture_tutorial_subtitle; } @Override @@ -85,9 +73,7 @@ final class BackGestureTutorialController extends TutorialController { public int getSuccessFeedbackSubtitle() { return mTutorialFragment.isAtFinalStep() ? R.string.back_gesture_feedback_complete_without_follow_up - : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.back_gesture_feedback_complete_with_follow_up - : R.string.back_gesture_feedback_complete_with_overview_follow_up; + : R.string.back_gesture_feedback_complete_with_follow_up; } @Override @@ -128,20 +114,12 @@ final class BackGestureTutorialController extends TutorialController { @LayoutRes int getMockAppTaskCurrentPageLayoutResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.back_gesture_tutorial_background - : mTutorialFragment.isLargeScreen() - ? R.layout.gesture_tutorial_tablet_mock_conversation - : R.layout.gesture_tutorial_mock_conversation; + return R.layout.back_gesture_tutorial_background; } @LayoutRes int getMockAppTaskPreviousPageLayoutResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.back_gesture_tutorial_background - : mTutorialFragment.isLargeScreen() - ? R.layout.gesture_tutorial_tablet_mock_conversation_list - : R.layout.gesture_tutorial_mock_conversation_list; + return R.layout.back_gesture_tutorial_background; } @Override @@ -156,7 +134,7 @@ final class BackGestureTutorialController extends TutorialController { @Override public void onBackGestureAttempted(BackGestureResult result) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } switch (mTutorialType) { @@ -174,7 +152,7 @@ final class BackGestureTutorialController extends TutorialController { @Override public void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } @@ -214,17 +192,13 @@ final class BackGestureTutorialController extends TutorialController { } private void handleBackAttempt(BackGestureResult result) { - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - resetViewsForBackGesture(); - } + resetViewsForBackGesture(); switch (result) { case BACK_COMPLETED_FROM_LEFT: case BACK_COMPLETED_FROM_RIGHT: mTutorialFragment.releaseFeedbackAnimation(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mExitingAppView.setVisibility(View.GONE); - } + mExitingAppView.setVisibility(View.GONE); updateFakeAppTaskViewLayout(getMockAppTaskPreviousPageLayoutResId()); showSuccessFeedback(); break; @@ -243,7 +217,7 @@ final class BackGestureTutorialController extends TutorialController { @Override public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } if (mTutorialType == BACK_NAVIGATION_COMPLETE) { diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java index 1b12be85df..dda80285cd 100644 --- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java +++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java @@ -15,8 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.content.Context; import android.content.res.Resources; import android.graphics.Point; @@ -31,9 +29,9 @@ import android.view.ViewGroup.LayoutParams; import androidx.annotation.Nullable; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.testing.shared.ResourceUtils; -import com.android.launcher3.util.DisplayController; /** * Utility class to handle edge swipes for back gestures. @@ -47,6 +45,7 @@ public class EdgeBackGestureHandler implements OnTouchListener { "gestures.back_timeout", 250); private final Context mContext; + private final DeviceProfile mDeviceProfile; private final Point mDisplaySize = new Point(); @@ -91,9 +90,10 @@ public class EdgeBackGestureHandler implements OnTouchListener { } }; - EdgeBackGestureHandler(Context context) { + EdgeBackGestureHandler(Context context, DeviceProfile deviceProfile) { final Resources res = context.getResources(); mContext = context; + mDeviceProfile = deviceProfile; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, @@ -118,8 +118,7 @@ public class EdgeBackGestureHandler implements OnTouchListener { // Add a nav bar panel window. mEdgeBackPanel = new EdgeBackGesturePanel(mContext, parent, createLayoutParams()); mEdgeBackPanel.setBackCallback(mBackCallback); - Point currentSize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize; - mDisplaySize.set(currentSize.x, currentSize.y); + mDisplaySize.set(mDeviceProfile.getDeviceProperties().getWidthPx(), mDeviceProfile.getDeviceProperties().getHeightPx()); mEdgeBackPanel.setDisplaySize(mDisplaySize); } } @@ -211,10 +210,8 @@ public class EdgeBackGestureHandler implements OnTouchListener { } } - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x, - ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel()); - } + mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x, + ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel()); // forward touch mEdgeBackPanel.onMotionEvent(ev); diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java index 36ea9269b4..e6206d01b2 100644 --- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java +++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java @@ -37,10 +37,10 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.StatsLogManager; import com.android.quickstep.TouchInteractionService.TISBinder; import com.android.quickstep.interaction.TutorialController.TutorialType; +import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.TISBindHelper; import java.util.ArrayList; @@ -54,8 +54,6 @@ public class GestureSandboxActivity extends FragmentActivity { static final String KEY_TUTORIAL_TYPE = "tutorial_type"; static final String KEY_GESTURE_COMPLETE = "gesture_complete"; static final String KEY_USE_TUTORIAL_MENU = "use_tutorial_menu"; - public static final double SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95; - public static final double SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05; @Nullable private TutorialType[] mTutorialSteps; private GestureSandboxFragment mCurrentFragment; @@ -80,9 +78,7 @@ public class GestureSandboxActivity extends FragmentActivity { Bundle args = savedInstanceState == null ? getIntent().getExtras() : savedInstanceState; boolean gestureComplete = args != null && args.getBoolean(KEY_GESTURE_COMPLETE, false); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - && args != null - && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) { + if (args != null && args.getBoolean(KEY_USE_TUTORIAL_MENU, false)) { mTutorialSteps = null; TutorialType tutorialTypeOverride = (TutorialType) args.get(KEY_TUTORIAL_TYPE); mCurrentFragment = tutorialTypeOverride == null @@ -102,9 +98,7 @@ public class GestureSandboxActivity extends FragmentActivity { .add(R.id.gesture_tutorial_fragment_container, mCurrentFragment) .commit(); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - correctUserOrientation(); - } + correctUserOrientation(); mTISBindHelper = new TISBindHelper(this, this::onTISConnected); initWindowInsets(); @@ -116,29 +110,18 @@ public class GestureSandboxActivity extends FragmentActivity { super.onConfigurationChanged(newConfig); // Ensure the prompt to rotate the screen is updated - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - correctUserOrientation(); - } + correctUserOrientation(); } private void initWindowInsets() { View root = findViewById(android.R.id.content); - root.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - updateExclusionRects(root); - } - }); + root.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + updateExclusionRects(root)); // Return CONSUMED if you don't want want the window insets to keep being // passed down to descendant views. - root.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { - @Override - public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { - return WindowInsets.CONSUMED; - } - }); + root.setOnApplyWindowInsetsListener((v, insets) -> WindowInsets.CONSUMED); } private void updateExclusionRects(View rootView) { @@ -168,12 +151,9 @@ public class GestureSandboxActivity extends FragmentActivity { private void correctUserOrientation() { DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.get( getApplicationContext()).getDeviceProfile(this); - if (deviceProfile.isTablet) { + if (deviceProfile.getDeviceProperties().isTablet()) { // The tutorial will work in either orientation if the height and width are similar - boolean isAspectRatioSquare = - deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND - && deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND; - boolean showRotationPrompt = !isAspectRatioSquare + boolean showRotationPrompt = !LayoutUtils.isAspectRatioSquare(deviceProfile.getDeviceProperties().getAspectRatio()) && getResources().getConfiguration().orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java index 679e926a9a..b059695871 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java @@ -15,8 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.graphics.PointF; import com.android.launcher3.R; @@ -34,47 +32,34 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll super(fragment, tutorialType); // Set the Lottie animation colors specifically for the Home gesture - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - LottieAnimationColorUtils.updateToArgbColors( - mAnimatedGestureDemonstration, - Map.of(".onSurfaceHome", fragment.mRootView.mColorOnSurfaceHome, - ".surfaceHome", fragment.mRootView.mColorSurfaceHome, - ".secondaryHome", fragment.mRootView.mColorSecondaryHome)); + LottieAnimationColorUtils.updateToArgbColors( + mAnimatedGestureDemonstration, + Map.of(".onSurfaceHome", fragment.mRootView.mColorOnSurfaceHome, + ".surfaceHome", fragment.mRootView.mColorSurfaceHome, + ".secondaryHome", fragment.mRootView.mColorSecondaryHome)); - LottieAnimationColorUtils.updateToArgbColors( - mCheckmarkAnimation, - Map.of(".checkmark", - Utilities.isDarkTheme(mContext) - ? fragment.mRootView.mColorOnSurfaceHome - : fragment.mRootView.mColorSecondaryHome, - ".checkmarkBackground", fragment.mRootView.mColorSurfaceHome)); - } + LottieAnimationColorUtils.updateToArgbColors( + mCheckmarkAnimation, + Map.of(".checkmark", + Utilities.isDarkTheme(mContext) + ? fragment.mRootView.mColorOnSurfaceHome + : fragment.mRootView.mColorSecondaryHome, + ".checkmarkBackground", fragment.mRootView.mColorSurfaceHome)); } @Override public int getIntroductionTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.home_gesture_tutorial_title - : R.string.home_gesture_intro_title; + return R.string.home_gesture_tutorial_title; } @Override public int getIntroductionSubtitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.home_gesture_tutorial_subtitle - : R.string.home_gesture_intro_subtitle; - } - - @Override - public int getSpokenIntroductionSubtitle() { - return R.string.home_gesture_spoken_intro_subtitle; + return R.string.home_gesture_tutorial_subtitle; } @Override public int getSuccessFeedbackTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.home_gesture_tutorial_success - : R.string.gesture_tutorial_nice; + return R.string.home_gesture_tutorial_success; } @Override @@ -144,7 +129,7 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll @Override public void onBackGestureAttempted(BackGestureResult result) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } switch (mTutorialType) { @@ -171,7 +156,7 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll @Override public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } switch (mTutorialType) { diff --git a/quickstep/src/com/android/quickstep/interaction/MenuFragment.java b/quickstep/src/com/android/quickstep/interaction/MenuFragment.java index 8ead3dd8bd..2d7a36f127 100644 --- a/quickstep/src/com/android/quickstep/interaction/MenuFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/MenuFragment.java @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.R; +import com.android.wm.shell.shared.TypefaceUtils; /** Displays the gesture nav tutorial menu. */ public final class MenuFragment extends GestureSandboxFragment { @@ -58,6 +59,19 @@ public final class MenuFragment extends GestureSandboxFragment { root.findViewById(R.id.gesture_tutorial_menu_done_button).setOnClickListener( v -> close()); + TypefaceUtils.setTypeface( + root.findViewById(R.id.gesture_tutorial_menu_home_button_text), + TypefaceUtils.FontFamily.GSF_DISPLAY_SMALL_EMPHASIZED); + TypefaceUtils.setTypeface( + root.findViewById(R.id.gesture_tutorial_menu_back_button_text), + TypefaceUtils.FontFamily.GSF_DISPLAY_SMALL_EMPHASIZED); + TypefaceUtils.setTypeface( + root.findViewById(R.id.gesture_tutorial_menu_overview_button_text), + TypefaceUtils.FontFamily.GSF_DISPLAY_SMALL_EMPHASIZED); + TypefaceUtils.setTypeface( + root.findViewById(R.id.gesture_tutorial_menu_done_button), + TypefaceUtils.FontFamily.GSF_LABEL_LARGE); + return root; } diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java index e4c3a7e6ec..33230b08b5 100644 --- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java +++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java @@ -33,6 +33,7 @@ import android.view.View.OnTouchListener; import androidx.annotation.Nullable; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.testing.shared.ResourceUtils; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.NavigationMode; @@ -58,15 +59,16 @@ public class NavBarGestureHandler implements OnTouchListener, @Nullable private NavBarGestureAttemptCallback mGestureCallback; - NavBarGestureHandler(Context context) { + NavBarGestureHandler(Context context, DeviceProfile deviceProfile) { mContext = context; - DisplayController.Info displayInfo = DisplayController.INSTANCE.get(mContext).getInfo(); - Point currentSize = displayInfo.currentSize; - mDisplaySize.set(currentSize.x, currentSize.y); - mSwipeUpTouchTracker = - new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/, - new NavBarPosition(NavigationMode.NO_BUTTON, displayInfo), - this); + mDisplaySize.set(deviceProfile.getDeviceProperties().getWidthPx(), deviceProfile.getDeviceProperties().getHeightPx()); + mSwipeUpTouchTracker = new TriggerSwipeUpTouchTracker( + context, + /* disableHorizontalSwipe= */ true, + new NavBarPosition( + NavigationMode.NO_BUTTON, + DisplayController.INSTANCE.get(mContext).getInfo()), + /* onSwipeUp= */ this); mMotionPauseDetector = new MotionPauseDetector(context); final Resources resources = context.getResources(); diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java index 05d32cc88c..ff0d6d1cc2 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java @@ -16,7 +16,6 @@ package com.android.quickstep.interaction; import static com.android.app.animation.Interpolators.ACCELERATE; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -49,46 +48,33 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont super(fragment, tutorialType); // Set the Lottie animation colors specifically for the Overview gesture - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - LottieAnimationColorUtils.updateToArgbColors( - mAnimatedGestureDemonstration, - Map.of(".onSurfaceOverview", fragment.mRootView.mColorOnSurfaceOverview, - ".surfaceOverview", fragment.mRootView.mColorSurfaceOverview, - ".secondaryOverview", fragment.mRootView.mColorSecondaryOverview)); + LottieAnimationColorUtils.updateToArgbColors( + mAnimatedGestureDemonstration, + Map.of(".onSurfaceOverview", fragment.mRootView.mColorOnSurfaceOverview, + ".surfaceOverview", fragment.mRootView.mColorSurfaceOverview, + ".secondaryOverview", fragment.mRootView.mColorSecondaryOverview)); - LottieAnimationColorUtils.updateToArgbColors( - mCheckmarkAnimation, - Map.of(".checkmark", - Utilities.isDarkTheme(mContext) - ? fragment.mRootView.mColorOnSurfaceOverview - : fragment.mRootView.mColorSecondaryOverview, - ".checkmarkBackground", fragment.mRootView.mColorSurfaceOverview)); - } + LottieAnimationColorUtils.updateToArgbColors( + mCheckmarkAnimation, + Map.of(".checkmark", + Utilities.isDarkTheme(mContext) + ? fragment.mRootView.mColorOnSurfaceOverview + : fragment.mRootView.mColorSecondaryOverview, + ".checkmarkBackground", fragment.mRootView.mColorSurfaceOverview)); } @Override public int getIntroductionTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.overview_gesture_tutorial_title - : R.string.overview_gesture_intro_title; + return R.string.overview_gesture_tutorial_title; } @Override public int getIntroductionSubtitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.overview_gesture_tutorial_subtitle - : R.string.overview_gesture_intro_subtitle; - } - - @Override - public int getSpokenIntroductionSubtitle() { - return R.string.overview_gesture_spoken_intro_subtitle; + return R.string.overview_gesture_tutorial_subtitle; } @Override public int getSuccessFeedbackTitle() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.string.overview_gesture_tutorial_success - : R.string.gesture_tutorial_nice; + return R.string.overview_gesture_tutorial_success; } @Override @@ -168,15 +154,12 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont @Override protected int getMockPreviousAppTaskThumbnailColor() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? mTutorialFragment.mRootView.mColorSurfaceContainer - : mContext.getResources().getColor( - R.color.gesture_tutorial_fake_previous_task_view_color); + return mTutorialFragment.mRootView.mColorSurfaceContainer; } @Override public void onBackGestureAttempted(BackGestureResult result) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } switch (mTutorialType) { @@ -203,7 +186,7 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont @Override public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } switch (mTutorialType) { @@ -224,11 +207,8 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont case OVERVIEW_GESTURE_COMPLETED: setGestureCompleted(); mTutorialFragment.releaseFeedbackAnimation(); - animateTaskViewToOverview(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()); + animateTaskViewToOverview(true); onMotionPaused(true /*arbitrary value*/); - if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - showSuccessFeedback(); - } break; case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION: case HOME_OR_OVERVIEW_CANCELLED: diff --git a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java index affedb9497..d733267d37 100644 --- a/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java +++ b/quickstep/src/com/android/quickstep/interaction/RootSandboxLayout.java @@ -15,16 +15,12 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Insets; -import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; -import android.view.View; import android.view.WindowInsets; import android.widget.RelativeLayout; @@ -39,10 +35,6 @@ import com.android.launcher3.Utilities; /** Root layout that TutorialFragment uses to intercept motion events. */ public class RootSandboxLayout extends RelativeLayout { - private final Rect mTempStepIndicatorBounds = new Rect(); - private final Rect mTempInclusionBounds = new Rect(); - private final Rect mTempExclusionBounds = new Rect(); - @ColorInt final int mColorSurfaceContainer; @ColorInt final int mColorOnSurfaceHome; @ColorInt final int mColorSurfaceHome; @@ -54,11 +46,6 @@ public class RootSandboxLayout extends RelativeLayout { @ColorInt final int mColorSurfaceOverview; @ColorInt final int mColorSecondaryOverview; - private View mFeedbackView; - private View mTutorialStepView; - private View mSkipButton; - private View mDoneButton; - public RootSandboxLayout(Context context) { this(context, null); } @@ -123,56 +110,4 @@ public class RootSandboxLayout extends RelativeLayout { return getHeight() + insets.top + insets.bottom; } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - return; - } - mFeedbackView = findViewById(R.id.gesture_tutorial_fragment_feedback_view); - mTutorialStepView = - mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); - mSkipButton = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_close_button); - mDoneButton = mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_action_button); - - mFeedbackView.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (mSkipButton.getVisibility() != VISIBLE - && mDoneButton.getVisibility() != VISIBLE) { - return; - } - // Either the skip or the done button is ever shown at once, never both. - boolean showingSkipButton = mSkipButton.getVisibility() == VISIBLE; - boolean isRTL = Utilities.isRtl(getContext().getResources()); - updateTutorialStepViewTranslation( - showingSkipButton ? mSkipButton : mDoneButton, - // Translate the step indicator away from whichever button is being - // shown. The skip button in on the left in LTR or on the right in RTL. - // The done button is on the right in LTR or left in RTL. - (showingSkipButton && !isRTL) || (!showingSkipButton && isRTL)); - }); - } - - private void updateTutorialStepViewTranslation( - @NonNull View anchorView, boolean translateToRight) { - mTempStepIndicatorBounds.set( - mTutorialStepView.getLeft(), - mTutorialStepView.getTop(), - mTutorialStepView.getRight(), - mTutorialStepView.getBottom()); - mTempInclusionBounds.set(0, 0, mFeedbackView.getWidth(), mFeedbackView.getHeight()); - mTempExclusionBounds.set( - anchorView.getLeft(), - anchorView.getTop(), - anchorView.getRight(), - anchorView.getBottom()); - - Utilities.translateOverlappingView( - mTutorialStepView, - mTempStepIndicatorBounds, - mTempInclusionBounds, - mTempExclusionBounds, - translateToRight ? Utilities.TRANSLATE_RIGHT : Utilities.TRANSLATE_LEFT); - } } diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java index db98457b68..3379f1f5c8 100644 --- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java @@ -34,6 +34,7 @@ import android.graphics.Outline; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; +import android.view.Display; import android.view.View; import android.view.ViewOutlineProvider; @@ -48,11 +49,10 @@ import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorListeners; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; import com.android.quickstep.GestureState; import com.android.quickstep.OverviewComponentObserver; -import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.RemoteTargetGluer; +import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.SwipeUpAnimationLogic; import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim; import com.android.quickstep.util.RecordingSurfaceTransaction; @@ -86,12 +86,9 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { super(tutorialFragment, tutorialType); - RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext); - OverviewComponentObserver observer = new OverviewComponentObserver(mContext, deviceState); - mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState, - new GestureState(observer, -1)); - observer.onDestroy(); - deviceState.destroy(); + mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, new GestureState( + OverviewComponentObserver.INSTANCE.get(mContext), Display.DEFAULT_DISPLAY, -1), + RotationTouchHelper.REPOSITORY_INSTANCE.get(mContext).get(Display.DEFAULT_DISPLAY)); DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext) .getDeviceProfile(mContext) @@ -127,9 +124,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { void resetTaskViews() { mFakeHotseatView.setVisibility(View.INVISIBLE); mFakeIconView.setVisibility(View.INVISIBLE); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFakeIconView.getBackground().setTint(getFakeTaskViewColor()); - } + mFakeIconView.getBackground().setTint(getFakeTaskViewColor()); if (mTutorialFragment.getActivity() != null) { int height = mTutorialFragment.getRootView().getFullscreenHeight(); int width = mTutorialFragment.getRootView().getWidth(); @@ -138,9 +133,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { mFakeTaskViewRadius = 0; mFakeTaskView.invalidateOutline(); mFakeTaskView.setVisibility(View.VISIBLE); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); - } + mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); mFakeTaskView.setAlpha(1); mFakePreviousTaskView.setVisibility(View.INVISIBLE); mFakePreviousTaskView.setAlpha(1); @@ -149,7 +142,6 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { mShowPreviousTasks = false; mRunningWindowAnim = null; } - void fadeOutFakeTaskView(boolean toOverviewFirst, @Nullable Runnable onEndRunnable) { fadeOutFakeTaskView( toOverviewFirst, @@ -173,7 +165,8 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation, boolean isReverse) { - PendingAnimation fadeAnim = new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS); + PendingAnimation fadeAnim = + new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS); fadeAnim.setFloat(mTaskViewSwipeUpAnimation .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCELERATE); if (resetViews) { @@ -192,7 +185,8 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); - Animator multiRowAnimation = mFakePreviousTaskView.createAnimationToMultiRowLayout(); + Animator multiRowAnimation = + mFakePreviousTaskView.createAnimationToMultiRowLayout(); if (multiRowAnimation != null) { multiRowAnimation.setDuration( @@ -252,7 +246,8 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { mFakePreviousTaskView.setVisibility(View.INVISIBLE); mFakeHotseatView.setVisibility(View.VISIBLE); mShowPreviousTasks = false; - RectFSpringAnim rectAnim = mTaskViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity); + RectFSpringAnim rectAnim = + mTaskViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity); // After home animation finishes, fade out and run onEndRunnable. PendingAnimation fadeAnim = new PendingAnimation(300); fadeAnim.setViewAlpha(mFakeIconView, 0, ACCELERATE); @@ -277,7 +272,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { @Override public void setNavBarGestureProgress(@Nullable Float displacement) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } if (mTutorialType == HOME_NAVIGATION_COMPLETE @@ -298,7 +293,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { @Override public void onMotionPaused(boolean unused) { - if (skipGestureAttempt()) { + if (isGestureCompleted()) { return; } if (mShowTasks) { @@ -306,9 +301,9 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { mFakePreviousTaskView.setTranslationX( -(2 * mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN)); mFakePreviousTaskView.animate() - .setDuration(300) - .translationX(-(mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN)) - .start(); + .setDuration(300) + .translationX(-(mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN)) + .start(); } mShowPreviousTasks = true; } @@ -316,13 +311,14 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { class ViewSwipeUpAnimation extends SwipeUpAnimationLogic { - ViewSwipeUpAnimation(Context context, RecentsAnimationDeviceState deviceState, - GestureState gestureState) { - super(context, deviceState, gestureState); + ViewSwipeUpAnimation(Context context, GestureState gestureState, + RotationTouchHelper rotationTouchHelper) { + super(context, gestureState, rotationTouchHelper); mRemoteTargetHandles[0] = new RemoteTargetGluer.RemoteTargetHandle( mRemoteTargetHandles[0].getTaskViewSimulator(), new FakeTransformParams()); - for (RemoteTargetGluer.RemoteTargetHandle handle : mTargetGluer.getRemoteTargetHandles()) { + for (RemoteTargetGluer.RemoteTargetHandle handle + : mTargetGluer.getRemoteTargetHandles()) { // Override home screen rotation preference so that home and overview animations // work properly handle.getTaskViewSimulator() @@ -334,7 +330,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { void initDp(DeviceProfile dp) { initTransitionEndpoints(dp); mRemoteTargetHandles[0].getTaskViewSimulator().setPreviewBounds( - new Rect(0, 0, dp.widthPx, dp.heightPx), dp.getInsets()); + new Rect(0, 0, dp.getDeviceProperties().getWidthPx(), dp.getDeviceProperties().getHeightPx()), dp.getInsets()); } @Override @@ -387,12 +383,10 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { false, /* isOpening */ mFakeIconView, mDp); mFakeIconView.setAlpha(1); - if (FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - int iconColor = ColorUtils.blendARGB( - getFakeTaskViewColor(), getHotseatIconColor(), progress); - mFakeIconView.getBackground().setTint(iconColor); - mFakeTaskView.setBackgroundColor(iconColor); - } + int iconColor = ColorUtils.blendARGB( + getFakeTaskViewColor(), getHotseatIconColor(), progress); + mFakeIconView.getBackground().setTint(iconColor); + mFakeTaskView.setBackgroundColor(iconColor); mFakeTaskView.setAlpha(getWindowAlpha(progress)); mFakePreviousTaskView.setAlpha(getWindowAlpha(progress)); } @@ -437,18 +431,20 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mFakePreviousTaskView.setVisibility(View.VISIBLE); - onMotionPaused(true /* arbitrary value */); + onMotionPaused(true /*arbitrary value*/); } }); return overviewSwipeAnimator; } + private Animator createFingerDotSwipeUpAnimator(float fingerDotStartTranslationY) { ValueAnimator swipeAnimator = ValueAnimator.ofFloat(0f, 1f); swipeAnimator.addUpdateListener(valueAnimator -> { - float gestureProgress = -fingerDotStartTranslationY * valueAnimator.getAnimatedFraction(); + float gestureProgress = + -fingerDotStartTranslationY * valueAnimator.getAnimatedFraction(); setNavBarGestureProgress(gestureProgress); mFingerDotView.setTranslationY(fingerDotStartTranslationY + gestureProgress); }); diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index b0a21c2b3e..875010ad23 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -19,28 +19,24 @@ import static android.view.View.GONE; import static android.view.View.NO_ID; import static android.view.View.inflate; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; - import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.RawRes; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Rect; -import android.graphics.drawable.AnimatedVectorDrawable; -import android.graphics.drawable.ColorDrawable; +import android.graphics.Typeface; import android.graphics.drawable.RippleDrawable; +import android.os.SystemProperties; +import android.provider.Settings; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.Button; import android.widget.FrameLayout; @@ -52,21 +48,20 @@ import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.content.res.AppCompatResources; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorListeners; +import com.android.launcher3.util.SettingsCache; import com.android.launcher3.views.ClipIconView; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback; import com.android.systemui.shared.system.QuickStepContract; +import com.android.wm.shell.Flags; +import com.android.wm.shell.shared.TypefaceUtils.FontFamily; import com.airbnb.lottie.LottieAnimationView; import com.airbnb.lottie.LottieComposition; @@ -82,12 +77,13 @@ abstract class TutorialController implements BackGestureAttemptCallback, private static final float FINGER_DOT_SMALL_SCALE = 0.7f; private static final int FINGER_DOT_ANIMATION_DURATION_MILLIS = 500; - private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips"; - private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips"; + private static final String SUW_THEME_SYSTEM_PROPERTY = "setupwizard.theme"; + private static final String GLIF_EXPRESSIVE_THEME = "glif_expressive"; + private static final String GLIF_EXPRESSIVE_LIGHT_THEME = "glif_expressive_light"; private static final int FEEDBACK_ANIMATION_MS = 133; - private static final int RIPPLE_VISIBLE_MS = 300; - private static final int GESTURE_ANIMATION_DELAY_MS = 1500; + private static final int SUBTITLE_ANNOUNCE_DELAY_MS = 3000; + private static final int DONE_BUTTON_ANNOUNCE_DELAY_MS = 4000; private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 3000; private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000; protected float mExitingAppEndingCornerRadius; @@ -100,7 +96,6 @@ abstract class TutorialController implements BackGestureAttemptCallback, TutorialType mTutorialType; final Context mContext; - final TextView mSkipButton; final Button mDoneButton; final ViewGroup mFeedbackView; final TextView mFeedbackTitleView; @@ -108,37 +103,29 @@ abstract class TutorialController implements BackGestureAttemptCallback, final ImageView mEdgeGestureVideoView; final RelativeLayout mFakeLauncherView; final FrameLayout mFakeHotseatView; - @Nullable - View mHotseatIconView; + @Nullable View mHotseatIconView; final ClipIconView mFakeIconView; final FrameLayout mFakeTaskView; - @Nullable - final AnimatedTaskbarView mFakeTaskbarView; + @Nullable final AnimatedTaskbarView mFakeTaskbarView; final AnimatedTaskView mFakePreviousTaskView; final View mRippleView; final RippleDrawable mRippleDrawable; - final TutorialStepIndicator mTutorialStepView; final ImageView mFingerDotView; private final Rect mExitingAppRect = new Rect(); protected View mExitingAppView; protected int mExitingAppRadius; - private final AlertDialog mSkipTutorialDialog; + private final boolean mIsExpressiveThemeEnabledInSUW; private boolean mGestureCompleted = false; protected LottieAnimationView mAnimatedGestureDemonstration; protected LottieAnimationView mCheckmarkAnimation; private RelativeLayout mFullGestureDemonstration; - // These runnables should be used when posting callbacks to their views and - // cleared from their + // These runnables should be used when posting callbacks to their views and cleared from their // views before posting new callbacks. - private final Runnable mTitleViewCallback; - @Nullable - private Runnable mFeedbackViewCallback; - @Nullable - private Runnable mFakeTaskViewCallback; - @Nullable - private Runnable mFakeTaskbarViewCallback; + @Nullable private Runnable mFeedbackViewCallback; + @Nullable private Runnable mFakeTaskViewCallback; + @Nullable private Runnable mFakeTaskbarViewCallback; private final Runnable mShowFeedbackRunnable; TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { @@ -147,8 +134,6 @@ abstract class TutorialController implements BackGestureAttemptCallback, mContext = mTutorialFragment.getContext(); RootSandboxLayout rootView = tutorialFragment.getRootView(); - mSkipButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button); - mSkipButton.setOnClickListener(button -> showSkipTutorialDialog()); mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view); mFeedbackTitleView = mFeedbackView.findViewById( R.id.gesture_tutorial_fragment_feedback_title); @@ -159,46 +144,44 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view); mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view); mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view); - mFakeTaskbarView = ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? null - : rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view); - mFakePreviousTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view); + mFakeTaskbarView = null; + mFakePreviousTaskView = + rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view); mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view); mRippleDrawable = (RippleDrawable) mRippleView.getBackground(); mDoneButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button); - mTutorialStepView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot); - mSkipTutorialDialog = createSkipTutorialDialog(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration); - mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation); - mAnimatedGestureDemonstration = rootView.findViewById( - R.id.gesture_demonstration_animations); - mExitingAppView = rootView.findViewById(R.id.exiting_app_back); - mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx; - mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx; - mExitingAppMargin = mContext.getResources().getDimensionPixelSize( - R.dimen.gesture_tutorial_back_gesture_exiting_app_margin); - mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext); - mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize( - R.dimen.gesture_tutorial_back_gesture_end_corner_radius); - mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener( - this::createScalingMatrix); + mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration); + mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation); + mAnimatedGestureDemonstration = rootView.findViewById( + R.id.gesture_demonstration_animations); + mExitingAppView = rootView.findViewById(R.id.exiting_app_back); + mScreenWidth = mTutorialFragment.getDeviceProfile().getDeviceProperties().getWidthPx(); + mScreenHeight = mTutorialFragment.getDeviceProfile().getDeviceProperties().getHeightPx(); + mExitingAppMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.gesture_tutorial_back_gesture_exiting_app_margin); + mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext); + mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize( + R.dimen.gesture_tutorial_back_gesture_end_corner_radius); + mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener( + this::createScalingMatrix); - mFeedbackTitleView.setText(getIntroductionTitle()); - mFeedbackSubtitleView.setText(getIntroductionSubtitle()); - mExitingAppView.setClipToOutline(true); - mExitingAppView.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(mExitingAppRect, mExitingAppRadius); - } - }); - } + mFeedbackTitleView.setText(getIntroductionTitle()); + mFeedbackSubtitleView.setText(getIntroductionSubtitle()); - mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent( - AccessibilityEvent.TYPE_VIEW_FOCUSED); + String SUWTheme = SystemProperties.get(SUW_THEME_SYSTEM_PROPERTY, ""); + mIsExpressiveThemeEnabledInSUW = SUWTheme.equals(GLIF_EXPRESSIVE_THEME) || SUWTheme.equals( + GLIF_EXPRESSIVE_LIGHT_THEME); + maybeSetTitleTypefaces(); + + mExitingAppView.setClipToOutline(true); + mExitingAppView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(mExitingAppRect, mExitingAppRadius); + } + }); mShowFeedbackRunnable = () -> { mFeedbackView.setAlpha(0f); mFeedbackView.setScaleX(0.95f); @@ -220,18 +203,15 @@ abstract class TutorialController implements BackGestureAttemptCallback, AccessibilityManager.getInstance(mContext) .getRecommendedTimeoutMillis( ADVANCE_TUTORIAL_TIMEOUT_MS, - AccessibilityManager.FLAG_CONTENT_TEXT)); + AccessibilityManager.FLAG_CONTENT_TEXT + | AccessibilityManager.FLAG_CONTENT_CONTROLS)); } }) .start(); - mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); }; } - /** - * Scale the Lottie gesture animation to fit the device based on device - * dimensions - */ + /** Scale the Lottie gesture animation to fit the device based on device dimensions */ private void createScalingMatrix(LottieComposition composition) { Rect animationBoundsRect = composition.getBounds(); if (animationBoundsRect == null) { @@ -247,22 +227,14 @@ abstract class TutorialController implements BackGestureAttemptCallback, mAnimatedGestureDemonstration.setImageMatrix(scaleMatrix); } - private void showSkipTutorialDialog() { - if (mSkipTutorialDialog != null) { - mSkipTutorialDialog.show(); - } - } - public int getHotseatIconTop() { return mHotseatIconView == null - ? 0 - : mFakeHotseatView.getTop() + mHotseatIconView.getTop(); + ? 0 : mFakeHotseatView.getTop() + mHotseatIconView.getTop(); } public int getHotseatIconLeft() { return mHotseatIconView == null - ? 0 - : mFakeHotseatView.getLeft() + mHotseatIconView.getLeft(); + ? 0 : mFakeHotseatView.getLeft() + mHotseatIconView.getLeft(); } void setTutorialType(TutorialType tutorialType) { @@ -271,19 +243,11 @@ abstract class TutorialController implements BackGestureAttemptCallback, @LayoutRes protected int getMockHotseatResId() { - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - return mTutorialFragment.isLargeScreen() - ? mTutorialFragment.isFoldable() - ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat - : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat - : R.layout.redesigned_gesture_tutorial_mock_hotseat; - } else { - return mTutorialFragment.isLargeScreen() - ? mTutorialFragment.isFoldable() - ? R.layout.gesture_tutorial_foldable_mock_hotseat - : R.layout.gesture_tutorial_tablet_mock_hotseat - : R.layout.gesture_tutorial_mock_hotseat; - } + return mTutorialFragment.isLargeScreen() + ? mTutorialFragment.isFoldable() + ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat + : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat + : R.layout.redesigned_gesture_tutorial_mock_hotseat; } @LayoutRes @@ -322,9 +286,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, @DrawableRes public int getMockAppIconResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.drawable.redesigned_hotseat_icon - : R.drawable.default_sandbox_app_icon; + return R.drawable.redesigned_hotseat_icon; } @DrawableRes @@ -346,11 +308,6 @@ abstract class TutorialController implements BackGestureAttemptCallback, return NO_ID; } - @StringRes - public int getSpokenIntroductionSubtitle() { - return NO_ID; - } - @StringRes public int getSuccessFeedbackSubtitle() { return NO_ID; @@ -384,16 +341,11 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFeedbackView.setTranslationY(0); return; } - Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); - AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); - if (gestureAnimation != null && edgeAnimation != null) { - playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true); - } + playFeedbackAnimation(); } /** - * Only use this when a gesture is completed, but the feedback shouldn't be - * shown immediately. + * Only use this when a gesture is completed, but the feedback shouldn't be shown immediately. * In that case, call this method immediately instead. */ public void setGestureCompleted() { @@ -406,8 +358,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, void showSuccessFeedback() { int successSubtitleResId = getSuccessFeedbackSubtitle(); if (successSubtitleResId == NO_ID) { - // Allow crash since this should never be reached with a tutorial controller - // used in + // Allow crash since this should never be reached with a tutorial controller used in // production. Log.e(LOG_TAG, "Cannot show success feedback for tutorial step: " + mTutorialType @@ -429,41 +380,42 @@ abstract class TutorialController implements BackGestureAttemptCallback, /** * Show feedback reflecting the result of a gesture attempt. * - * @param isGestureSuccessful Whether the tutorial feedback's action button - * should be shown. + * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown. **/ void showFeedback(int subtitleResId, boolean isGestureSuccessful) { showFeedback( isGestureSuccessful - ? getSuccessFeedbackTitle() - : R.string.gesture_tutorial_try_again, + ? getSuccessFeedbackTitle() : R.string.gesture_tutorial_try_again, subtitleResId, - NO_ID, - isGestureSuccessful, - false); + isGestureSuccessful); } void showFeedback( int titleResId, int subtitleResId, - int spokenSubtitleResId, - boolean isGestureSuccessful, - boolean useGestureAnimationDelay) { - mFeedbackTitleView.removeCallbacks(mTitleViewCallback); + boolean isGestureSuccessful) { if (mFeedbackViewCallback != null) { mFeedbackView.removeCallbacks(mFeedbackViewCallback); mFeedbackViewCallback = null; } mFeedbackTitleView.setText(titleResId); - mFeedbackSubtitleView.setText( - ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() || spokenSubtitleResId == NO_ID - ? mContext.getText(subtitleResId) - : Utilities.wrapForTts( - mContext.getText(subtitleResId), - mContext.getString(spokenSubtitleResId))); + mFeedbackSubtitleView.setText(subtitleResId); + + boolean isUserSetupComplete = SettingsCache.INSTANCE.get(mContext).getValue( + Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), 0); + boolean userSetupNotCompleteAndExpressiveThemeEnabled = + !isUserSetupComplete && mIsExpressiveThemeEnabledInSUW; + boolean userSetupCompleteAndNewFontsEnabled = isUserSetupComplete && Flags.enableGsf(); + if (isGestureSuccessful) { if (mTutorialFragment.isAtFinalStep()) { + if (userSetupCompleteAndNewFontsEnabled + || userSetupNotCompleteAndExpressiveThemeEnabled) { + mDoneButton.setTypeface( + Typeface.create(FontFamily.GSF_LABEL_LARGE.getValue(), + Typeface.NORMAL)); + } showActionButton(); } @@ -472,44 +424,30 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeTaskViewCallback = null; } - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - showSuccessPage(); - } + showSuccessPage(); } mGestureCompleted = isGestureSuccessful; - - Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); - AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); - if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) { - playFeedbackAnimation( - gestureAnimation, - edgeAnimation, - mShowFeedbackRunnable, - useGestureAnimationDelay); - return; + if (!isGestureSuccessful) { + playFeedbackAnimation(); } else { mTutorialFragment.releaseFeedbackAnimation(); + mFeedbackViewCallback = mShowFeedbackRunnable; + mFeedbackView.post(mFeedbackViewCallback); } - mFeedbackViewCallback = mShowFeedbackRunnable; - - mFeedbackView.post(mFeedbackViewCallback); } private void showSuccessPage() { pauseAndHideLottieAnimation(); mCheckmarkAnimation.setVisibility(View.VISIBLE); mCheckmarkAnimation.playAnimation(); - mFeedbackTitleView.setTextAppearance(mContext, getSuccessTitleTextAppearance()); + mFeedbackTitleView.setTextAppearance(getSuccessTitleTextAppearance()); + maybeSetTitleTypefaces(); } public boolean isGestureCompleted() { return mGestureCompleted; } - public boolean skipGestureAttempt() { - return isGestureCompleted() || mTutorialFragment.isRotationPromptShowing(); - } - void hideFeedback() { if (mFeedbackView.getVisibility() != View.VISIBLE) { return; @@ -532,111 +470,42 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); mFakeTaskbarViewCallback = null; } - mFeedbackTitleView.removeCallbacks(mTitleViewCallback); } - private void playFeedbackAnimation( - @NonNull Animator gestureAnimation, - @NonNull AnimatedVectorDrawable edgeAnimation, - @NonNull Runnable onStartRunnable, - boolean useGestureAnimationDelay) { - - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFeedbackView.setVisibility(View.VISIBLE); - mAnimatedGestureDemonstration.setVisibility(View.VISIBLE); - mFullGestureDemonstration.setVisibility(View.VISIBLE); - mAnimatedGestureDemonstration.playAnimation(); - return; - } - - if (gestureAnimation.isRunning()) { - gestureAnimation.cancel(); - } - if (edgeAnimation.isRunning()) { - edgeAnimation.reset(); - } - gestureAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - - mEdgeGestureVideoView.setVisibility(GONE); - if (edgeAnimation.isRunning()) { - edgeAnimation.stop(); - } - - if (!useGestureAnimationDelay) { - onStartRunnable.run(); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - - mEdgeGestureVideoView.setVisibility(View.VISIBLE); - edgeAnimation.start(); - - gestureAnimation.removeListener(this); - } - }); - - cancelQueuedGestureAnimation(); - if (useGestureAnimationDelay) { - mFeedbackViewCallback = onStartRunnable; - mFakeTaskViewCallback = gestureAnimation::start; - - mFeedbackView.post(mFeedbackViewCallback); - mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS); - } else { - gestureAnimation.start(); - } + private void playFeedbackAnimation() { + mFeedbackView.setVisibility(View.VISIBLE); + mAnimatedGestureDemonstration.setVisibility(View.VISIBLE); + mFullGestureDemonstration.setVisibility(View.VISIBLE); + mAnimatedGestureDemonstration.playAnimation(); } void setRippleHotspot(float x, float y) { mRippleDrawable.setHotspot(x, y); } - void showRippleEffect(@Nullable Runnable onCompleteRunnable) { - mRippleDrawable.setState( - new int[] { android.R.attr.state_pressed, android.R.attr.state_enabled }); - mRippleView.postDelayed(() -> { - mRippleDrawable.setState(new int[] {}); - if (onCompleteRunnable != null) { - onCompleteRunnable.run(); - } - }, RIPPLE_VISIBLE_MS); - } - void onActionButtonClicked(View button) { mTutorialFragment.continueTutorial(); } @CallSuper void transitToController() { - updateCloseButton(); - updateSubtext(); updateDrawables(); updateLayout(); - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance()); - mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance()); - mDoneButton.getBackground().setTint(getDoneButtonColor()); - mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep() - ? R.raw.checkmark_animation_end - : R.raw.checkmark_animation_in_progress); - if (!isGestureCompleted()) { - mCheckmarkAnimation.setVisibility(GONE); - startGestureAnimation(); - if (mTutorialType == TutorialType.BACK_NAVIGATION) { - resetViewsForBackGesture(); - } + mFeedbackTitleView.setTextAppearance(getTitleTextAppearance()); + mDoneButton.setTextAppearance(getDoneButtonTextAppearance()); + maybeSetTitleTypefaces(); + mDoneButton.getBackground().setTint(getDoneButtonColor()); + mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep() + ? R.raw.checkmark_animation_end + : R.raw.checkmark_animation_in_progress); + if (!isGestureCompleted()) { + mCheckmarkAnimation.setVisibility(GONE); + startGestureAnimation(); + if (mTutorialType == TutorialType.BACK_NAVIGATION) { + resetViewsForBackGesture(); } - } else { - hideFeedback(); - hideActionButton(); } mGestureCompleted = false; @@ -645,6 +514,20 @@ abstract class TutorialController implements BackGestureAttemptCallback, } } + /** + * Apply expressive typefaces to the feedback title and subtitle views. + */ + private void maybeSetTitleTypefaces() { + if (mIsExpressiveThemeEnabledInSUW || Flags.enableGsf()) { + mFeedbackTitleView.setTypeface(Typeface.create(mTutorialFragment.isLargeScreen() + ? FontFamily.GSF_DISPLAY_MEDIUM_EMPHASIZED.getValue() + : FontFamily.GSF_DISPLAY_SMALL_EMPHASIZED.getValue(), + Typeface.NORMAL)); + mFeedbackSubtitleView.setTypeface( + Typeface.create(FontFamily.GSF_BODY_LARGE.getValue(), Typeface.NORMAL)); + } + } + protected void resetViewsForBackGesture() { mFakeTaskView.setVisibility(View.VISIBLE); mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); @@ -666,21 +549,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, mAnimatedGestureDemonstration.playAnimation(); } - void updateCloseButton() { - mSkipButton.setTextAppearance(Utilities.isDarkTheme(mContext) - ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext - : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark); - } - - void hideActionButton() { - mSkipButton.setVisibility(View.VISIBLE); - // Invisible to maintain the layout. - mDoneButton.setVisibility(View.INVISIBLE); - mDoneButton.setOnClickListener(null); - } - void showActionButton() { - mSkipButton.setVisibility(GONE); mDoneButton.setVisibility(View.VISIBLE); mDoneButton.setOnClickListener(this::onActionButtonClicked); } @@ -693,7 +562,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); } if (animateToHotseat) { - mFakeTaskbarViewCallback = () -> mFakeTaskbarView.animateDisappearanceToHotseat(mFakeHotseatView); + mFakeTaskbarViewCallback = () -> + mFakeTaskbarView.animateDisappearanceToHotseat(mFakeHotseatView); } mFakeTaskbarView.post(mFakeTaskbarViewCallback); } @@ -706,7 +576,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); } if (animateFromHotseat) { - mFakeTaskbarViewCallback = () -> mFakeTaskbarView.animateAppearanceFromHotseat(mFakeHotseatView); + mFakeTaskbarViewCallback = () -> + mFakeTaskbarView.animateAppearanceFromHotseat(mFakeHotseatView); } mFakeTaskbarView.post(mFakeTaskbarViewCallback); } @@ -726,48 +597,32 @@ abstract class TutorialController implements BackGestureAttemptCallback, } } - private void updateSubtext() { - if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mTutorialStepView.setTutorialProgress( - mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps()); - } - } - private void updateHotseatChildViewColor(@Nullable View child) { - if (child == null) - return; + if (child == null) return; child.getBackground().setTint(getHotseatIconColor()); } private void updateDrawables() { if (mContext != null) { - mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable( - mContext, getMockWallpaperResId())); + mTutorialFragment.getRootView() + .setBackground(mContext.getDrawable(getMockWallpaperResId())); mTutorialFragment.updateFeedbackAnimation(); - mFakeLauncherView.setBackgroundColor(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? getFakeLauncherColor() - : mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); + mFakeLauncherView.setBackgroundColor(getFakeLauncherColor()); updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId()); mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1); mFakeTaskView.animate().alpha(1).setListener( AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel())); mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor()); - mFakeIconView.setBackground(AppCompatResources.getDrawable( - mContext, getMockAppIconResId())); - - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mExitingAppView.setBackgroundColor(getExitingAppColor()); - mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); - updateHotseatChildViewColor(mHotseatIconView); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6)); - updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar)); - } else { - updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId()); - } + mFakeIconView.setBackground(mContext.getDrawable(getMockAppIconResId())); + mExitingAppView.setBackgroundColor(getExitingAppColor()); + mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); + updateHotseatChildViewColor(mHotseatIconView); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6)); + updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar)); } } @@ -775,8 +630,8 @@ abstract class TutorialController implements BackGestureAttemptCallback, if (mContext == null) { return; } - RelativeLayout.LayoutParams feedbackLayoutParams = (RelativeLayout.LayoutParams) mFeedbackView - .getLayoutParams(); + RelativeLayout.LayoutParams feedbackLayoutParams = + (RelativeLayout.LayoutParams) mFeedbackView.getLayoutParams(); feedbackLayoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize( mTutorialFragment.isLargeScreen() ? R.dimen.gesture_tutorial_tablet_feedback_margin_start_end @@ -795,13 +650,12 @@ abstract class TutorialController implements BackGestureAttemptCallback, mTutorialFragment.isLargeScreen() ? View.VISIBLE : GONE); } - RelativeLayout.LayoutParams hotseatLayoutParams = (RelativeLayout.LayoutParams) mFakeHotseatView - .getLayoutParams(); + RelativeLayout.LayoutParams hotseatLayoutParams = + (RelativeLayout.LayoutParams) mFakeHotseatView.getLayoutParams(); if (!mTutorialFragment.isLargeScreen()) { DeviceProfile dp = mTutorialFragment.getDeviceProfile(); - dp.updateIsSeascape(mContext); - hotseatLayoutParams.addRule(dp.isLandscape + hotseatLayoutParams.addRule(dp.getDeviceProperties().isLandscape() ? (dp.isSeascape() ? RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END) @@ -816,68 +670,6 @@ abstract class TutorialController implements BackGestureAttemptCallback, mFakeHotseatView.setLayoutParams(hotseatLayoutParams); } - private AlertDialog createSkipTutorialDialog() { - if (!(mContext instanceof GestureSandboxActivity)) { - return null; - } - GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext; - View contentView = View.inflate( - sandboxActivity, R.layout.gesture_tutorial_dialog, null); - AlertDialog tutorialDialog = new AlertDialog.Builder(sandboxActivity, - androidx.appcompat.R.style.Base_Theme_AppCompat_Dialog) - .setView(contentView) - .create(); - - PackageManager packageManager = mContext.getPackageManager(); - CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME; - - try { - tipsAppName = packageManager.getApplicationLabel( - packageManager.getApplicationInfo( - PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(LOG_TAG, - "Could not find app label for package name: " - + PIXEL_TIPS_APP_PACKAGE_NAME - + ". Defaulting to 'Pixel Tips.'", - e); - } - - TextView subtitleTextView = (TextView) contentView.findViewById( - R.id.gesture_tutorial_dialog_subtitle); - if (subtitleTextView != null) { - subtitleTextView.setText( - mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName)); - } else { - Log.w(LOG_TAG, "No subtitle view in the skip tutorial dialog to update."); - } - - Button cancelButton = (Button) contentView.findViewById( - R.id.gesture_tutorial_dialog_cancel_button); - if (cancelButton != null) { - cancelButton.setOnClickListener( - v -> tutorialDialog.dismiss()); - } else { - Log.w(LOG_TAG, "No cancel button in the skip tutorial dialog to update."); - } - - Button confirmButton = contentView.findViewById( - R.id.gesture_tutorial_dialog_confirm_button); - if (confirmButton != null) { - confirmButton.setOnClickListener(v -> { - mTutorialFragment.closeTutorialStep(true); - tutorialDialog.dismiss(); - }); - } else { - Log.w(LOG_TAG, "No confirm button in the skip tutorial dialog to update."); - } - - tutorialDialog.getWindow().setBackgroundDrawable( - new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent))); - - return tutorialDialog; - } - protected AnimatorSet createFingerDotAppearanceAnimatorSet() { ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( mFingerDotView, View.ALPHA, 0f, FINGER_DOT_VISIBLE_ALPHA); diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java index 9dc4e3968c..a6c46ddd64 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java @@ -17,7 +17,6 @@ package com.android.quickstep.interaction; import static android.view.View.NO_ID; -import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_GESTURE_COMPLETE; import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_TUTORIAL_TYPE; import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_USE_TUTORIAL_MENU; @@ -49,6 +48,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.logging.StatsLogManager; import com.android.quickstep.interaction.TutorialController.TutorialType; @@ -176,13 +176,15 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE); mGestureComplete = args.getBoolean(KEY_GESTURE_COMPLETE, false); - mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext()); - mNavBarGestureHandler = new NavBarGestureHandler(getContext()); + DeviceProfile deviceProfile = LauncherAppState.getInstance(getContext()) + .getInvariantDeviceProfile().getDeviceProfile(getContext()); + mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext(), deviceProfile); + mNavBarGestureHandler = new NavBarGestureHandler(getContext(), deviceProfile); mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(getContext()) .getDeviceProfile(getContext()); - mIsLargeScreen = mDeviceProfile.isTablet; - mIsFoldable = mDeviceProfile.isTwoPanels; + mIsLargeScreen = mDeviceProfile.getDeviceProperties().isTablet(); + mIsFoldable = mDeviceProfile.getDeviceProperties().isTwoPanels(); if (mOnAttachedToWindowPendingCreate) { mOnAttachedToWindowPendingCreate = false; @@ -213,11 +215,8 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc public View onCreateView( @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); - mRootView = (RootSandboxLayout) inflater.inflate( - ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.redesigned_gesture_tutorial_fragment - : R.layout.gesture_tutorial_fragment, + R.layout.redesigned_gesture_tutorial_fragment, container, false); @@ -271,9 +270,7 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc mTutorialController.showFeedback( introTitleResId, introSubtitleResId, - mTutorialController.getSpokenIntroductionSubtitle(), - false, - true); + /* isGestureSuccessful= */ false); mIntroductionShown = true; } } @@ -383,10 +380,7 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc if (mTutorialController != null && !isGestureComplete()) { mTutorialController.hideFeedback(); } - - if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { - mTutorialController.pauseAndHideLottieAnimation(); - } + mTutorialController.pauseAndHideLottieAnimation(); // Note: Using logical-or to ensure both functions get called. return mEdgeBackGestureHandler.onTouch(view, motionEvent) diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java deleted file mode 100644 index ae0e725aad..0000000000 --- a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.interaction; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; -import android.widget.ImageView; -import android.widget.LinearLayout; - -import androidx.appcompat.content.res.AppCompatResources; - -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.icons.GraphicsUtils; - -/** Indicator displaying the current progress through the gesture navigation tutorial. */ -public class TutorialStepIndicator extends LinearLayout { - - private static final String LOG_TAG = "TutorialStepIndicator"; - - private int mCurrentStep = -1; - private int mTotalSteps = -1; - - public TutorialStepIndicator(Context context) { - super(context); - } - - public TutorialStepIndicator(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public TutorialStepIndicator(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public TutorialStepIndicator(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - /** - * Updates this indicator to display totalSteps indicator pills, with the first currentStep - * pills highlighted. - */ - public void setTutorialProgress(int currentStep, int totalSteps) { - if (currentStep <= 0) { - Log.w(LOG_TAG, "Current step number invalid: " + currentStep + ". Assuming step 1."); - currentStep = 1; - } - if (totalSteps <= 0) { - Log.w(LOG_TAG, "Total number of steps invalid: " + totalSteps + ". Assuming 1 step."); - totalSteps = 1; - } - if (currentStep > totalSteps) { - Log.w(LOG_TAG, "Current step number greater than the total number of steps. Assuming" - + " final step."); - currentStep = totalSteps; - } - if (totalSteps < 2) { - setVisibility(GONE); - return; - } - setVisibility(VISIBLE); - mCurrentStep = currentStep; - mTotalSteps = totalSteps; - - initializeStepIndicators(); - } - - private void initializeStepIndicators() { - for (int i = mTotalSteps; i < getChildCount(); i++) { - removeViewAt(i); - } - int activeStepIndicatorColor = GraphicsUtils.getAttrColor( - getContext(), android.R.attr.textColorPrimary); - int inactiveStepIndicatorColor = GraphicsUtils.getAttrColor( - getContext(), android.R.attr.textColorSecondaryInverse); - for (int i = 0; i < mTotalSteps; i++) { - Drawable pageIndicatorPillDrawable = AppCompatResources.getDrawable( - getContext(), R.drawable.tutorial_step_indicator_pill); - - if (i >= getChildCount()) { - ImageView pageIndicatorPill = new ImageView(getContext()); - pageIndicatorPill.setImageDrawable(pageIndicatorPillDrawable); - - LinearLayout.LayoutParams lp = new LayoutParams( - LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - - lp.setMarginStart(Utilities.dpToPx(3)); - lp.setMarginEnd(Utilities.dpToPx(3)); - - addView(pageIndicatorPill, lp); - } - if (pageIndicatorPillDrawable != null) { - if (i < mCurrentStep) { - pageIndicatorPillDrawable.setTint(activeStepIndicatorColor); - } else { - pageIndicatorPillDrawable.setTint(inactiveStepIndicatorColor); - } - } - } - } -} diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java index 5ac04da0ac..c9d1207a07 100644 --- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java +++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java @@ -16,20 +16,29 @@ package com.android.quickstep.logging; -import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; import static com.android.launcher3.LauncherPrefs.getDevicePrefs; import static com.android.launcher3.LauncherPrefs.getPrefs; +import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE; +import static com.android.launcher3.graphics.ThemeManager.THEMED_ICONS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ICON_SHAPE_ARCH; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ICON_SHAPE_CIRCLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ICON_SHAPE_FOUR_SIDED_COOKIE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ICON_SHAPE_SEVEN_SIDED_COOKIE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ICON_SHAPE_SQUARE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_DISABLED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_ENABLED; -import static com.android.launcher3.model.DeviceGridState.KEY_WORKSPACE_SIZE; import static com.android.launcher3.model.PredictionUpdateTask.LAST_PREDICTION_ENABLED; +import static com.android.launcher3.shapes.ShapesProvider.ARCH_KEY; +import static com.android.launcher3.shapes.ShapesProvider.CIRCLE_KEY; +import static com.android.launcher3.shapes.ShapesProvider.FOUR_SIDED_COOKIE_KEY; +import static com.android.launcher3.shapes.ShapesProvider.SEVEN_SIDED_COOKIE_KEY; +import static com.android.launcher3.shapes.ShapesProvider.SQUARE_KEY; import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI; -import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS; import android.content.Context; import android.content.SharedPreferences; @@ -39,19 +48,26 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Xml; -import com.android.launcher3.AutoInstallsLayout; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.Flags; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.logging.StatsLogManager.LauncherEvent; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.DeviceGridState; +import com.android.launcher3.util.DaggerSingletonObject; +import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.Info; -import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.NavigationMode; -import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SettingsCache; +import com.android.quickstep.dagger.QuickstepBaseAppComponent; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -59,45 +75,60 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.Optional; +import javax.inject.Inject; + /** * Utility class to log launcher settings changes */ +@LauncherAppSingleton public class SettingsChangeLogger implements - DisplayController.DisplayInfoChangeListener, OnSharedPreferenceChangeListener, - SafeCloseable { + DisplayController.DisplayInfoChangeListener, OnSharedPreferenceChangeListener { /** * Singleton instance */ - public static MainThreadInitializedObject INSTANCE = - new MainThreadInitializedObject<>(SettingsChangeLogger::new); + public static DaggerSingletonObject INSTANCE = + new DaggerSingletonObject<>(QuickstepBaseAppComponent::getSettingsChangeLogger); private static final String TAG = "SettingsChangeLogger"; - private static final String ROOT_TAG = "androidx.preference.PreferenceScreen"; private static final String BOOLEAN_PREF = "SwitchPreference"; private final Context mContext; private final ArrayMap mLoggablePrefs; private final StatsLogManager mStatsLogManager; + @NonNull private NavigationMode mNavMode; - private StatsLogManager.LauncherEvent mNotificationDotsEvent; - private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent; + private LauncherEvent mNotificationDotsEvent; - private SettingsChangeLogger(Context context) { + private final SettingsCache.OnChangeListener mListener = this::onNotificationDotsChanged; + + @Inject + SettingsChangeLogger(@ApplicationContext Context context, + DaggerSingletonTracker tracker, + DisplayController displayController, + SettingsCache settingsCache, + StatsLogManager.StatsLogManagerFactory factory) { mContext = context; - mStatsLogManager = StatsLogManager.newInstance(mContext); + mStatsLogManager = factory.create(context); mLoggablePrefs = loadPrefKeys(context); - DisplayController.INSTANCE.get(context).addChangeListener(this); - mNavMode = DisplayController.getNavigationMode(context); + + displayController.addChangeListener(this); + mNavMode = displayController.getInfo().getNavigationMode(); + tracker.addCloseable(() -> displayController.removeChangeListener(this)); getPrefs(context).registerOnSharedPreferenceChangeListener(this); getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this); + tracker.addCloseable(() -> { + getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this); + getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this); + }); - SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context); - mSettingsCache.register(NOTIFICATION_BADGING_URI, - this::onNotificationDotsChanged); - onNotificationDotsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI)); + settingsCache.register(NOTIFICATION_BADGING_URI, mListener); + onNotificationDotsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI)); + tracker.addCloseable(() -> { + settingsCache.unregister(NOTIFICATION_BADGING_URI, mListener); + }); } private static ArrayMap loadPrefKeys(Context context) { @@ -105,7 +136,13 @@ public class SettingsChangeLogger implements ArrayMap result = new ArrayMap<>(); try { - AutoInstallsLayout.beginDocument(parser, ROOT_TAG); + // Move cursor to first tag because it could be + // androidx.preference.PreferenceScreen or PreferenceScreen + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG + && eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + } final int depth = parser.getDepth(); int type; while (((type = parser.next()) != XmlPullParser.END_TAG @@ -134,7 +171,7 @@ public class SettingsChangeLogger implements } private void onNotificationDotsChanged(boolean isDotsEnabled) { - StatsLogManager.LauncherEvent mEvent = + LauncherEvent mEvent = isDotsEnabled ? LAUNCHER_NOTIFICATION_DOT_ENABLED : LAUNCHER_NOTIFICATION_DOT_DISABLED; @@ -155,19 +192,22 @@ public class SettingsChangeLogger implements @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { - if (LAST_PREDICTION_ENABLED.getSharedPrefKey().equals(key) - || KEY_WORKSPACE_SIZE.equals(key) - || KEY_THEMED_ICONS.equals(key) - || mLoggablePrefs.containsKey(key)) { - - mHomeScreenSuggestionEvent = LauncherPrefs.get(mContext).get(LAST_PREDICTION_ENABLED) - ? LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED - : LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED; - - mStatsLogManager.logger().log(mHomeScreenSuggestionEvent); + LoggablePref loggablePref; + if (LAST_PREDICTION_ENABLED.getSharedPrefKey().equals(key)) { + logHomeScreenSuggestionEvent(mStatsLogManager.logger()); + } else if ((loggablePref = mLoggablePrefs.get(key)) != null) { + int eventId = prefs.getBoolean(key, loggablePref.defaultValue) + ? loggablePref.eventIdOn : loggablePref.eventIdOff; + mStatsLogManager.logger().log(() -> eventId); } } + private void logHomeScreenSuggestionEvent(StatsLogger logger) { + logger.log(LauncherPrefs.get(mContext).get(LAST_PREDICTION_ENABLED) + ? LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED + : LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED); + } + /** * Takes snapshot of all eligible launcher settings and log them with the provided instance ID. */ @@ -175,8 +215,8 @@ public class SettingsChangeLogger implements StatsLogger logger = mStatsLogManager.logger().withInstanceId(snapshotInstanceId); Optional.ofNullable(mNotificationDotsEvent).ifPresent(logger::log); - Optional.ofNullable(mNavMode).map(mode -> mode.launcherEvent).ifPresent(logger::log); - Optional.ofNullable(mHomeScreenSuggestionEvent).ifPresent(logger::log); + logger.log(mNavMode.launcherEvent); + logHomeScreenSuggestionEvent(logger); Optional.ofNullable(new DeviceGridState(mContext).getWorkspaceSizeEvent()).ifPresent( logger::log); @@ -185,17 +225,30 @@ public class SettingsChangeLogger implements ? LAUNCHER_THEMED_ICON_ENABLED : LAUNCHER_THEMED_ICON_DISABLED); + if (Flags.enableLauncherIconShapes()) { + Optional.ofNullable( + switch (LauncherPrefs.get(mContext).get(PREF_ICON_SHAPE)) { + case CIRCLE_KEY -> LAUNCHER_ICON_SHAPE_CIRCLE; + case SQUARE_KEY -> LAUNCHER_ICON_SHAPE_SQUARE; + case FOUR_SIDED_COOKIE_KEY -> LAUNCHER_ICON_SHAPE_FOUR_SIDED_COOKIE; + case SEVEN_SIDED_COOKIE_KEY -> LAUNCHER_ICON_SHAPE_SEVEN_SIDED_COOKIE; + case ARCH_KEY -> LAUNCHER_ICON_SHAPE_ARCH; + default -> null; + } + ).ifPresent(logger::log); + } + mLoggablePrefs.forEach((key, lp) -> logger.log(() -> prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff)); } - @Override - public void close() { - getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this); - getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this); + @VisibleForTesting + ArrayMap getLoggingPrefs() { + return mLoggablePrefs; } - private static class LoggablePref { + @VisibleForTesting + static class LoggablePref { public boolean defaultValue; public int eventIdOn; public int eventIdOff; diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java index 4225516e77..e69de29bb2 100644 --- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java +++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java @@ -1,918 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep.logging; - -import static android.view.Surface.ROTATION_180; -import static android.view.Surface.ROTATION_270; -import static android.view.Surface.ROTATION_90; - -import static androidx.core.util.Preconditions.checkNotNull; -import static androidx.core.util.Preconditions.checkState; - -import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_NON_ACTIONABLE; -import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.ALL_APPS_CONTAINER; -import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.EXTENDED_CONTAINERS; -import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER; -import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER; -import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_0; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_90; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_180; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_270; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__ALLAPPS; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__PORTRAIT; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__LANDSCAPE; -import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__SEASCAPE; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import android.util.StatsEvent; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; -import androidx.slice.SliceItem; - -import com.android.internal.jank.Cuj; -import com.android.launcher3.LauncherAppState; -import com.android.launcher3.Utilities; -import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.logger.LauncherAtom.Attribute; -import com.android.launcher3.logger.LauncherAtom.ContainerInfo; -import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase; -import com.android.launcher3.logger.LauncherAtom.FolderIcon; -import com.android.launcher3.logger.LauncherAtom.FromState; -import com.android.launcher3.logger.LauncherAtom.LauncherAttributes; -import com.android.launcher3.logger.LauncherAtom.ToState; -import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer; -import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer.SearchAttributes; -import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers; -import com.android.launcher3.logging.InstanceId; -import com.android.launcher3.logging.StatsLogManager; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.Executors; -import com.android.launcher3.util.LogConfig; -import com.android.launcher3.views.ActivityContext; -import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.systemui.shared.system.SysUiStatsLog; - -import java.util.Optional; -import java.util.OptionalInt; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * This class calls StatsLog compile time generated methods. - * - * To see if the logs are properly sent to statsd, execute following command. - *

    - * $ wwdebug (to turn on the logcat printout) - * $ wwlogcat (see logcat with grep filter on) - * $ statsd_testdrive (see how ww is writing the proto to statsd buffer) - *
- */ -public class StatsLogCompatManager extends StatsLogManager { - - private static final String TAG = "StatsLog"; - private static final String LATENCY_TAG = "StatsLatencyLog"; - private static final String IMPRESSION_TAG = "StatsImpressionLog"; - private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG); - private static final boolean DEBUG = !Utilities.isRunningInTestHarness(); - private static final InstanceId DEFAULT_INSTANCE_ID = InstanceId.fakeInstanceId(0); - // LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher - // proto migrates - // from nano to lite, bake constant to prevent robo test failure. - private static final int DEFAULT_PAGE_INDEX = -2; - private static final int FOLDER_HIERARCHY_OFFSET = 100; - private static final int SEARCH_RESULT_HIERARCHY_OFFSET = 200; - private static final int EXTENDED_CONTAINERS_HIERARCHY_OFFSET = 300; - private static final int ALL_APPS_HIERARCHY_OFFSET = 400; - - /** - * Flags for converting SearchAttribute to integer value. - */ - private static final int SEARCH_ATTRIBUTES_CORRECTED_QUERY = 1 << 0; - private static final int SEARCH_ATTRIBUTES_DIRECT_MATCH = 1 << 1; - private static final int SEARCH_ATTRIBUTES_ENTRY_STATE_ALL_APPS = 1 << 2; - private static final int SEARCH_ATTRIBUTES_ENTRY_STATE_QSB = 1 << 3; - private static final int SEARCH_ATTRIBUTES_ENTRY_STATE_OVERVIEW = 1 << 4; - private static final int SEARCH_ATTRIBUTES_ENTRY_STATE_TASKBAR = 1 << 5; - - public static final CopyOnWriteArrayList LOGS_CONSUMER = new CopyOnWriteArrayList<>(); - - public StatsLogCompatManager(Context context) { - super(context); - } - - @Override - protected StatsLogger createLogger() { - return new StatsCompatLogger(mContext, mActivityContext); - } - - @Override - protected StatsLatencyLogger createLatencyLogger() { - return new StatsCompatLatencyLogger(); - } - - @Override - protected StatsImpressionLogger createImpressionLogger() { - return new StatsCompatImpressionLogger(); - } - - /** - * Synchronously writes an itemInfo to stats log - */ - @WorkerThread - public static void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) { - if (IS_VERBOSE) { - Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info)); - } - if (Utilities.isRunningInTestHarness() || !Utilities.ATLEAST_R) { - return; - } - SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_SNAPSHOT, - LAUNCHER_WORKSPACE_SNAPSHOT.getId() /* event_id */, - info.getItemCase().getNumber() /* target_id */, - instanceId.getId() /* instance_id */, - 0 /* uid */, - getPackageName(info) /* package_name */, - getComponentName(info) /* component_name */, - getGridX(info, false) /* grid_x */, - getGridY(info, false) /* grid_y */, - getPageId(info) /* page_id */, - getGridX(info, true) /* grid_x_parent */, - getGridY(info, true) /* grid_y_parent */, - getParentPageId(info) /* page_id_parent */, - getHierarchy(info) /* hierarchy */, - info.getIsWork() /* is_work_profile */, - 0 /* origin */, - getCardinality(info) /* cardinality */, - info.getWidget().getSpanX(), - info.getWidget().getSpanY(), - getFeatures(info), - getAttributes(info) /* attributes */ - ); - } - - private static byte[] getAttributes(LauncherAtom.ItemInfo itemInfo) { - LauncherAttributes.Builder responseBuilder = LauncherAttributes.newBuilder(); - itemInfo.getItemAttributesList().stream().map(Attribute::getNumber).forEach( - responseBuilder::addItemAttributes); - return responseBuilder.build().toByteArray(); - } - - /** - * Builds {@link StatsEvent} from {@link LauncherAtom.ItemInfo}. Used for pulled - * atom callback - * implementation. - */ - public static StatsEvent buildStatsEvent(LauncherAtom.ItemInfo info, - @Nullable InstanceId instanceId) { - return SysUiStatsLog.buildStatsEvent( - SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT, // atom ID, - LAUNCHER_WORKSPACE_SNAPSHOT.getId(), // event_id = 1; - info.getItemCase().getNumber(), // item_id = 2; - instanceId == null ? 0 : instanceId.getId(), // instance_id = 3; - 0, // uid = 4 [(is_uid) = true]; - getPackageName(info), // package_name = 5; - getComponentName(info), // component_name = 6; - getGridX(info, false), // grid_x = 7 [default = -1]; - getGridY(info, false), // grid_y = 8 [default = -1]; - getPageId(info), // page_id = 9 [default = -2]; - getGridX(info, true), // grid_x_parent = 10 [default = -1]; - getGridY(info, true), // grid_y_parent = 11 [default = -1]; - getParentPageId(info), // page_id_parent = 12 [default = -2]; - getHierarchy(info), // container_id = 13; - info.getIsWork(), // is_work_profile = 14; - 0, // attribute_id = 15; - getCardinality(info), // cardinality = 16; - info.getWidget().getSpanX(), // span_x = 17 [default = 1]; - info.getWidget().getSpanY(), // span_y = 18 [default = 1]; - getAttributes(info) /* attributes = 19 [(log_mode) = MODE_BYTES] */, - info.getIsKidsMode() /* is_kids_mode = 20 */ - ); - } - - /** - * Helps to construct and write statsd compatible log message. - */ - private static class StatsCompatLogger implements StatsLogger { - - private static final ItemInfo DEFAULT_ITEM_INFO = new ItemInfo(); - static { - DEFAULT_ITEM_INFO.itemType = ITEM_TYPE_NON_ACTIONABLE; - } - private final Context mContext; - private final Optional mActivityContext; - private ItemInfo mItemInfo = DEFAULT_ITEM_INFO; - private InstanceId mInstanceId = DEFAULT_INSTANCE_ID; - private OptionalInt mRank = OptionalInt.empty(); - private Optional mContainerInfo = Optional.empty(); - private int mSrcState = LAUNCHER_STATE_UNSPECIFIED; - private int mDstState = LAUNCHER_STATE_UNSPECIFIED; - private Optional mFromState = Optional.empty(); - private Optional mToState = Optional.empty(); - private Optional mEditText = Optional.empty(); - private SliceItem mSliceItem; - private LauncherAtom.Slice mSlice; - private Optional mCardinality = Optional.empty(); - private int mInputType = SysUiStatsLog.LAUNCHER_UICHANGED__INPUT_TYPE__UNKNOWN; - private Optional mFeatures = Optional.empty(); - private Optional mPackageName = Optional.empty(); - /** - * Indicates the current rotation of the display. Uses {@link android.view.Surface values.} - */ - private final int mDisplayRotation; - - StatsCompatLogger(Context context, ActivityContext activityContext) { - mContext = context; - mActivityContext = Optional.ofNullable(activityContext); - mDisplayRotation = DisplayController.INSTANCE.get(mContext).getInfo().rotation; - } - - @Override - public StatsLogger withItemInfo(ItemInfo itemInfo) { - if (mContainerInfo.isPresent()) { - throw new IllegalArgumentException( - "ItemInfo and ContainerInfo are mutual exclusive; cannot log both."); - } - this.mItemInfo = itemInfo; - return this; - } - - @Override - public StatsLogger withInstanceId(InstanceId instanceId) { - this.mInstanceId = instanceId; - return this; - } - - @Override - public StatsLogger withRank(int rank) { - this.mRank = OptionalInt.of(rank); - return this; - } - - @Override - public StatsLogger withSrcState(int srcState) { - this.mSrcState = srcState; - return this; - } - - @Override - public StatsLogger withDstState(int dstState) { - this.mDstState = dstState; - return this; - } - - @Override - public StatsLogger withContainerInfo(ContainerInfo containerInfo) { - checkState(mItemInfo == DEFAULT_ITEM_INFO, - "ItemInfo and ContainerInfo are mutual exclusive; cannot log both."); - this.mContainerInfo = Optional.of(containerInfo); - return this; - } - - @Override - public StatsLogger withFromState(FromState fromState) { - this.mFromState = Optional.of(fromState); - return this; - } - - @Override - public StatsLogger withToState(ToState toState) { - this.mToState = Optional.of(toState); - return this; - } - - @Override - public StatsLogger withEditText(String editText) { - this.mEditText = Optional.of(editText); - return this; - } - - @Override - public StatsLogger withSliceItem(@NonNull SliceItem sliceItem) { - checkState(mItemInfo == DEFAULT_ITEM_INFO && mSlice == null, - "ItemInfo, Slice and SliceItem are mutual exclusive; cannot set more than one" - + " of them."); - this.mSliceItem = checkNotNull(sliceItem, "expected valid sliceItem but received null"); - return this; - } - - @Override - public StatsLogger withSlice(LauncherAtom.Slice slice) { - checkState(mItemInfo == DEFAULT_ITEM_INFO && mSliceItem == null, - "ItemInfo, Slice and SliceItem are mutual exclusive; cannot set more than one" - + " of them."); - checkNotNull(slice, "expected valid slice but received null"); - checkNotNull(slice.getUri(), "expected valid slice uri but received null"); - this.mSlice = slice; - return this; - } - - @Override - public StatsLogger withCardinality(int cardinality) { - this.mCardinality = Optional.of(cardinality); - return this; - } - - @Override - public StatsLogger withInputType(int inputType) { - this.mInputType = inputType; - return this; - } - - @Override - public StatsLogger withFeatures(int feature) { - this.mFeatures = Optional.of(feature); - return this; - } - - @Override - public StatsLogger withPackageName(@Nullable String packageName) { - mPackageName = Optional.ofNullable(packageName); - return this; - } - - @Override - public void log(EventEnum event) { - if (DEBUG) { - String name = (event instanceof Enum) ? ((Enum) event).name() : event.getId() + ""; - Log.d(TAG, name); - } - - if (mSlice == null && mSliceItem != null) { - mSlice = LauncherAtom.Slice.newBuilder().setUri( - mSliceItem.getSlice().getUri().toString()).build(); - } - - if (mSlice != null) { - Executors.MODEL_EXECUTOR.execute( - () -> { - LauncherAtom.ItemInfo.Builder itemInfoBuilder = LauncherAtom.ItemInfo.newBuilder() - .setSlice(mSlice); - mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo); - write(event, applyOverwrites(itemInfoBuilder.build())); - }); - return; - } - - if (mItemInfo == null) { - return; - } - - if (mItemInfo.container < 0 || !LauncherAppState.INSTANCE.executeIfCreated(app -> { - // Item is inside a collection, fetch collection info in a BG thread - // and then write to StatsLog. - app.getModel().enqueueModelUpdateTask((taskController, dataModel, apps) -> - write(event, applyOverwrites(mItemInfo.buildProto( - dataModel.collections.get(mItemInfo.container))))); - })) { - // Write log on the model thread so that logs do not go out of order - // (for eg: drop comes after drag) - Executors.MODEL_EXECUTOR.execute( - () -> write(event, applyOverwrites(mItemInfo.buildProto()))); - } - } - - @Override - public void sendToInteractionJankMonitor(EventEnum event, View view) { - if (!(event instanceof LauncherEvent)) { - return; - } - switch ((LauncherEvent) event) { - case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN: - InteractionJankMonitorWrapper.begin( - view, - Cuj.CUJ_LAUNCHER_ALL_APPS_SCROLL); - break; - case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END: - InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_ALL_APPS_SCROLL); - break; - case LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN: - InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_LOCK); - break; - case LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END: - InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_LOCK); - break; - case LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN: - InteractionJankMonitorWrapper.begin( - view, - Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK); - break; - case LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END: - InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK); - break; - default: - break; - } - } - - private LauncherAtom.ItemInfo applyOverwrites(LauncherAtom.ItemInfo atomInfo) { - LauncherAtom.ItemInfo.Builder itemInfoBuilder = atomInfo.toBuilder(); - - mRank.ifPresent(itemInfoBuilder::setRank); - mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo); - - mActivityContext.ifPresent(activityContext -> activityContext.applyOverwritesToLogItem(itemInfoBuilder)); - - if (mFromState.isPresent() || mToState.isPresent() || mEditText.isPresent()) { - FolderIcon.Builder folderIconBuilder = itemInfoBuilder - .getFolderIcon() - .toBuilder(); - mFromState.ifPresent(folderIconBuilder::setFromLabelState); - mToState.ifPresent(folderIconBuilder::setToLabelState); - mEditText.ifPresent(folderIconBuilder::setLabelInfo); - itemInfoBuilder.setFolderIcon(folderIconBuilder); - } - return itemInfoBuilder.build(); - } - - @WorkerThread - private void write(EventEnum event, LauncherAtom.ItemInfo atomInfo) { - InstanceId instanceId = mInstanceId; - int srcState = mSrcState; - int dstState = mDstState; - int inputType = mInputType; - String packageName = mPackageName.orElseGet(() -> getPackageName(atomInfo)); - if (IS_VERBOSE) { - String name = (event instanceof Enum) ? ((Enum) event).name() : event.getId() + ""; - StringBuilder logStringBuilder = new StringBuilder("\n"); - if (instanceId != DEFAULT_INSTANCE_ID) { - logStringBuilder.append(String.format("InstanceId:%s ", instanceId)); - } - logStringBuilder.append(name); - if (srcState != LAUNCHER_STATE_UNSPECIFIED - || dstState != LAUNCHER_STATE_UNSPECIFIED) { - logStringBuilder.append( - String.format("(State:%s->%s)", getStateString(srcState), - getStateString(dstState))); - } - if (atomInfo.hasContainerInfo()) { - logStringBuilder.append("\n").append(atomInfo); - } - if (!TextUtils.isEmpty(packageName)) { - logStringBuilder.append(String.format("\nPackage name: %s", packageName)); - } - Log.d(TAG, logStringBuilder.toString()); - } - - for (StatsLogConsumer consumer : LOGS_CONSUMER) { - consumer.consume(event, atomInfo); - } - - // TODO: remove this when b/231648228 is fixed. - if (Utilities.isRunningInTestHarness() || !Utilities.ATLEAST_R) { - return; - } - int cardinality = mCardinality.orElseGet(() -> getCardinality(atomInfo)); - int features = mFeatures.orElseGet(() -> getFeatures(atomInfo)); - SysUiStatsLog.write( - SysUiStatsLog.LAUNCHER_EVENT, - SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */, - srcState, - dstState, - null /* launcher extensions, deprecated */, - false /* quickstep_enabled, deprecated */, - event.getId() /* event_id */, - atomInfo.getItemCase().getNumber() /* target_id */, - instanceId.getId() /* instance_id TODO */, - 0 /* uid TODO */, - packageName /* package_name */, - getComponentName(atomInfo) /* component_name */, - getGridX(atomInfo, false) /* grid_x */, - getGridY(atomInfo, false) /* grid_y */, - getPageId(atomInfo) /* page_id */, - getGridX(atomInfo, true) /* grid_x_parent */, - getGridY(atomInfo, true) /* grid_y_parent */, - getParentPageId(atomInfo) /* page_id_parent */, - getHierarchy(atomInfo) /* hierarchy */, - false /* is_work_profile, deprecated */, - atomInfo.getRank() /* rank */, - atomInfo.getFolderIcon().getFromLabelState().getNumber() /* fromState */, - atomInfo.getFolderIcon().getToLabelState().getNumber() /* toState */, - atomInfo.getFolderIcon().getLabelInfo() /* edittext */, - cardinality /* cardinality */, - features /* features */, - getSearchAttributes(atomInfo) /* searchAttributes */, - getAttributes(atomInfo) /* attributes */, - inputType /* input_type */, - atomInfo.getUserType() /* user_type */, - getDisplayRotation() /* display_rotation */, - getRecentsOrientationHandler(atomInfo) /* recents_orientation_handler */); - } - - private int getDisplayRotation() { - return switch (mDisplayRotation) { - case ROTATION_90 -> LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_90; - case ROTATION_180 -> LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_180; - case ROTATION_270 -> LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_270; - default -> LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_0; - }; - } - - private int getRecentsOrientationHandler(LauncherAtom.ItemInfo itemInfo) { - var orientationHandler = - itemInfo.getContainerInfo().getTaskSwitcherContainer().getOrientationHandler(); - return switch (orientationHandler) { - case PORTRAIT -> LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__PORTRAIT; - case LANDSCAPE -> LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__LANDSCAPE; - case SEASCAPE -> LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__SEASCAPE; - }; - } - } - - /** - * Helps to construct and log statsd compatible latency events. - */ - private static class StatsCompatLatencyLogger implements StatsLatencyLogger { - private InstanceId mInstanceId = DEFAULT_INSTANCE_ID; - private LatencyType mType = LatencyType.UNKNOWN; - private int mPackageId = 0; - private long mLatencyInMillis; - private int mQueryLength = -1; - private int mSubEventType = 0; - private int mCardinality = -1; - - @Override - public StatsLatencyLogger withInstanceId(InstanceId instanceId) { - this.mInstanceId = instanceId; - return this; - } - - @Override - public StatsLatencyLogger withType(LatencyType type) { - this.mType = type; - return this; - } - - @Override - public StatsLatencyLogger withPackageId(int packageId) { - this.mPackageId = packageId; - return this; - } - - @Override - public StatsLatencyLogger withLatency(long latencyInMillis) { - this.mLatencyInMillis = latencyInMillis; - return this; - } - - @Override - public StatsLatencyLogger withQueryLength(int queryLength) { - this.mQueryLength = queryLength; - return this; - } - - @Override - public StatsLatencyLogger withSubEventType(int type) { - this.mSubEventType = type; - return this; - } - - @Override - public StatsLatencyLogger withCardinality(int cardinality) { - this.mCardinality = cardinality; - return this; - } - - @Override - public void log(EventEnum event) { - if (IS_VERBOSE) { - String name = (event instanceof Enum) ? ((Enum) event).name() : event.getId() + ""; - StringBuilder logStringBuilder = new StringBuilder("\n"); - logStringBuilder.append(String.format("InstanceId:%s ", mInstanceId)); - logStringBuilder.append(String.format("%s=%sms", name, mLatencyInMillis)); - Log.d(LATENCY_TAG, logStringBuilder.toString()); - } - - SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_LATENCY, - event.getId(), // event_id - mInstanceId.getId(), // instance_id - mPackageId, // package_id - mLatencyInMillis, // latency_in_millis - mType.getId(), // type - mQueryLength, // query_length - mSubEventType, // sub_event_type - mCardinality // cardinality - ); - } - } - - /** - * Helps to construct and log statsd compatible impression events. - */ - private static class StatsCompatImpressionLogger implements StatsImpressionLogger { - private InstanceId mInstanceId = DEFAULT_INSTANCE_ID; - private State mLauncherState = State.UNKNOWN; - private int mQueryLength = -1; - - // Fields used for Impression Logging V2. - private int mResultType; - private boolean mAboveKeyboard = false; - private int mUid; - private int mResultSource; - - @Override - public StatsImpressionLogger withInstanceId(InstanceId instanceId) { - this.mInstanceId = instanceId; - return this; - } - - @Override - public StatsImpressionLogger withState(State state) { - this.mLauncherState = state; - return this; - } - - @Override - public StatsImpressionLogger withQueryLength(int queryLength) { - this.mQueryLength = queryLength; - return this; - } - - @Override - public StatsImpressionLogger withResultType(int resultType) { - mResultType = resultType; - return this; - } - - @Override - public StatsImpressionLogger withAboveKeyboard(boolean aboveKeyboard) { - mAboveKeyboard = aboveKeyboard; - return this; - } - - @Override - public StatsImpressionLogger withUid(int uid) { - mUid = uid; - return this; - } - - @Override - public StatsImpressionLogger withResultSource(int resultSource) { - mResultSource = resultSource; - return this; - } - - @Override - public void log(EventEnum event) { - if (IS_VERBOSE) { - String name = (event instanceof Enum) ? ((Enum) event).name() : event.getId() + ""; - StringBuilder logStringBuilder = new StringBuilder("\n"); - logStringBuilder.append(String.format("InstanceId:%s ", mInstanceId)); - logStringBuilder.append(String.format("ImpressionEvent:%s ", name)); - logStringBuilder.append(String.format("\n\tLauncherState = %s ", mLauncherState)); - logStringBuilder.append(String.format("\tQueryLength = %s ", mQueryLength)); - logStringBuilder.append(String.format( - "\n\t ResultType = %s is_above_keyboard = %s" - + " uid = %s result_source = %s", - mResultType, - mAboveKeyboard, mUid, mResultSource)); - - Log.d(IMPRESSION_TAG, logStringBuilder.toString()); - } - - SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT_V2, - event.getId(), // event_id - mInstanceId.getId(), // instance_id - mLauncherState.getLauncherState(), // state - mQueryLength, // query_length - mResultType, // result type - mAboveKeyboard, // above keyboard - mUid, // uid - mResultSource // result source - - ); - } - } - - private static int getCardinality(LauncherAtom.ItemInfo info) { - if (Utilities.isRunningInTestHarness()) { - return 0; - } - switch (info.getContainerInfo().getContainerCase()) { - case PREDICTED_HOTSEAT_CONTAINER: - return info.getContainerInfo().getPredictedHotseatContainer().getCardinality(); - case TASK_BAR_CONTAINER: - return info.getContainerInfo().getTaskBarContainer().getCardinality(); - case SEARCH_RESULT_CONTAINER: - return info.getContainerInfo().getSearchResultContainer().getQueryLength(); - case EXTENDED_CONTAINERS: - ExtendedContainers extendedCont = info.getContainerInfo().getExtendedContainers(); - if (extendedCont.getContainerCase() == DEVICE_SEARCH_RESULT_CONTAINER) { - DeviceSearchResultContainer deviceSearchResultCont = extendedCont - .getDeviceSearchResultContainer(); - return deviceSearchResultCont.hasQueryLength() ? deviceSearchResultCont - .getQueryLength() : -1; - } - default: - return info.getFolderIcon().getCardinality(); - } - } - - private static String getPackageName(LauncherAtom.ItemInfo info) { - switch (info.getItemCase()) { - case APPLICATION: - return info.getApplication().getPackageName(); - case SHORTCUT: - return info.getShortcut().getShortcutName(); - case WIDGET: - return info.getWidget().getPackageName(); - case TASK: - return info.getTask().getPackageName(); - case SEARCH_ACTION_ITEM: - return info.getSearchActionItem().getPackageName(); - default: - return null; - } - } - - private static String getComponentName(LauncherAtom.ItemInfo info) { - switch (info.getItemCase()) { - case APPLICATION: - return info.getApplication().getComponentName(); - case SHORTCUT: - return info.getShortcut().getShortcutName(); - case WIDGET: - return info.getWidget().getComponentName(); - case TASK: - return info.getTask().getComponentName(); - case SEARCH_ACTION_ITEM: - return info.getSearchActionItem().getTitle(); - case SLICE: - return info.getSlice().getUri(); - default: - return null; - } - } - - private static int getGridX(LauncherAtom.ItemInfo info, boolean parent) { - LauncherAtom.ContainerInfo containerInfo = info.getContainerInfo(); - if (containerInfo.getContainerCase() == FOLDER) { - if (parent) { - return containerInfo.getFolder().getWorkspace().getGridX(); - } else { - return containerInfo.getFolder().getGridX(); - } - } else if (containerInfo.getContainerCase() == EXTENDED_CONTAINERS) { - return containerInfo.getExtendedContainers() - .getDeviceSearchResultContainer().getGridX(); - } else { - return containerInfo.getWorkspace().getGridX(); - } - } - - private static int getGridY(LauncherAtom.ItemInfo info, boolean parent) { - if (info.getContainerInfo().getContainerCase() == FOLDER) { - if (parent) { - return info.getContainerInfo().getFolder().getWorkspace().getGridY(); - } else { - return info.getContainerInfo().getFolder().getGridY(); - } - } else { - return info.getContainerInfo().getWorkspace().getGridY(); - } - } - - private static int getPageId(LauncherAtom.ItemInfo info) { - if (info.hasTask()) { - return info.getTask().getIndex(); - } - switch (info.getContainerInfo().getContainerCase()) { - case FOLDER: - return info.getContainerInfo().getFolder().getPageIndex(); - case HOTSEAT: - return info.getContainerInfo().getHotseat().getIndex(); - case PREDICTED_HOTSEAT_CONTAINER: - return info.getContainerInfo().getPredictedHotseatContainer().getIndex(); - case TASK_BAR_CONTAINER: - return info.getContainerInfo().getTaskBarContainer().getIndex(); - default: - return info.getContainerInfo().getWorkspace().getPageIndex(); - } - } - - private static int getParentPageId(LauncherAtom.ItemInfo info) { - switch (info.getContainerInfo().getContainerCase()) { - case FOLDER: - if (info.getContainerInfo().getFolder().getParentContainerCase() == ParentContainerCase.HOTSEAT) { - return info.getContainerInfo().getFolder().getHotseat().getIndex(); - } - return info.getContainerInfo().getFolder().getWorkspace().getPageIndex(); - case SEARCH_RESULT_CONTAINER: - return info.getContainerInfo().getSearchResultContainer().getWorkspace() - .getPageIndex(); - default: - return info.getContainerInfo().getWorkspace().getPageIndex(); - } - } - - private static int getHierarchy(LauncherAtom.ItemInfo info) { - if (Utilities.isRunningInTestHarness()) { - return 0; - } - if (info.getContainerInfo().getContainerCase() == FOLDER) { - return info.getContainerInfo().getFolder().getParentContainerCase().getNumber() - + FOLDER_HIERARCHY_OFFSET; - } else if (info.getContainerInfo().getContainerCase() == SEARCH_RESULT_CONTAINER) { - return info.getContainerInfo().getSearchResultContainer().getParentContainerCase() - .getNumber() + SEARCH_RESULT_HIERARCHY_OFFSET; - } else if (info.getContainerInfo().getContainerCase() == EXTENDED_CONTAINERS) { - return info.getContainerInfo().getExtendedContainers().getContainerCase().getNumber() - + EXTENDED_CONTAINERS_HIERARCHY_OFFSET; - } else if (info.getContainerInfo().getContainerCase() == ALL_APPS_CONTAINER) { - return info.getContainerInfo().getAllAppsContainer().getParentContainerCase() - .getNumber() + ALL_APPS_HIERARCHY_OFFSET; - } else { - return info.getContainerInfo().getContainerCase().getNumber(); - } - } - - private static String getStateString(int state) { - switch (state) { - case LAUNCHER_UICHANGED__DST_STATE__BACKGROUND: - return "BACKGROUND"; - case LAUNCHER_UICHANGED__DST_STATE__HOME: - return "HOME"; - case LAUNCHER_UICHANGED__DST_STATE__OVERVIEW: - return "OVERVIEW"; - case LAUNCHER_UICHANGED__DST_STATE__ALLAPPS: - return "ALLAPPS"; - default: - return "INVALID"; - } - } - - private static int getFeatures(LauncherAtom.ItemInfo info) { - if (info.getItemCase().equals(LauncherAtom.ItemInfo.ItemCase.WIDGET)) { - return info.getWidget().getWidgetFeatures(); - } - return 0; - } - - private static int getSearchAttributes(LauncherAtom.ItemInfo info) { - if (Utilities.isRunningInTestHarness()) { - return 0; - } - ContainerInfo containerInfo = info.getContainerInfo(); - if (containerInfo.getContainerCase() == EXTENDED_CONTAINERS - && containerInfo.getExtendedContainers().getContainerCase() == DEVICE_SEARCH_RESULT_CONTAINER - && containerInfo.getExtendedContainers() - .getDeviceSearchResultContainer().hasSearchAttributes()) { - return searchAttributesToInt(containerInfo.getExtendedContainers() - .getDeviceSearchResultContainer().getSearchAttributes()); - } - return 0; - } - - private static int searchAttributesToInt(SearchAttributes searchAttributes) { - int response = 0; - if (searchAttributes.getCorrectedQuery()) { - response = response | SEARCH_ATTRIBUTES_CORRECTED_QUERY; - } - if (searchAttributes.getDirectMatch()) { - response = response | SEARCH_ATTRIBUTES_DIRECT_MATCH; - } - if (searchAttributes.getEntryState() == SearchAttributes.EntryState.ALL_APPS) { - response = response | SEARCH_ATTRIBUTES_ENTRY_STATE_ALL_APPS; - } else if (searchAttributes.getEntryState() == SearchAttributes.EntryState.QSB) { - response = response | SEARCH_ATTRIBUTES_ENTRY_STATE_QSB; - } else if (searchAttributes.getEntryState() == SearchAttributes.EntryState.OVERVIEW) { - response = response | SEARCH_ATTRIBUTES_ENTRY_STATE_OVERVIEW; - } else if (searchAttributes.getEntryState() == SearchAttributes.EntryState.TASKBAR) { - response = response | SEARCH_ATTRIBUTES_ENTRY_STATE_TASKBAR; - } - - return response; - } - - /** - * Interface to get stats log while it is dispatched to the system - */ - public interface StatsLogConsumer { - - @WorkerThread - void consume(EventEnum event, LauncherAtom.ItemInfo atomInfo); - } -} diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.kt b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.kt new file mode 100644 index 0000000000..e1d35fdffe --- /dev/null +++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.kt @@ -0,0 +1,798 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.logging + +import android.content.Context +import android.util.Log +import android.util.StatsEvent +import android.view.Surface +import android.view.View +import androidx.annotation.WorkerThread +import androidx.slice.SliceItem +import com.android.internal.jank.Cuj +import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherSettings.Favorites +import com.android.launcher3.Utilities +import com.android.launcher3.logger.LauncherAtom +import com.android.launcher3.logger.LauncherAtom.ContainerInfo +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.ALL_APPS_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.EXTENDED_CONTAINERS +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.HOTSEAT +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.PREDICTED_HOTSEAT_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER +import com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.TASK_BAR_CONTAINER +import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase +import com.android.launcher3.logger.LauncherAtom.FromState +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.APPLICATION +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.FOLDER_ICON +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.SEARCH_ACTION_ITEM +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.SHORTCUT +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.SLICE +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.TASK +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.TASK_VIEW +import com.android.launcher3.logger.LauncherAtom.ItemInfo.ItemCase.WIDGET +import com.android.launcher3.logger.LauncherAtom.LauncherAttributes +import com.android.launcher3.logger.LauncherAtom.Slice +import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer.OrientationHandler.LANDSCAPE +import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer.OrientationHandler.PORTRAIT +import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer.OrientationHandler.SEASCAPE +import com.android.launcher3.logger.LauncherAtom.ToState +import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer.SearchAttributes +import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer.SearchAttributes.EntryState.ALL_APPS +import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer.SearchAttributes.EntryState.OVERVIEW +import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer.SearchAttributes.EntryState.QSB +import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer.SearchAttributes.EntryState.TASKBAR +import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER +import com.android.launcher3.logging.InstanceId +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END +import com.android.launcher3.logging.StatsLogManager.StatsImpressionLogger.State +import com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType +import com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.UNKNOWN +import com.android.launcher3.model.data.CollectionInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.Executors +import com.android.launcher3.util.LogConfig +import com.android.launcher3.views.ActivityContext +import com.android.systemui.shared.system.InteractionJankMonitorWrapper +import com.android.systemui.shared.system.SysUiStatsLog +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject + +/** + * This class calls StatsLog compile time generated methods. + * + * To see if the logs are properly sent to statsd, execute following command. + * + * $ wwdebug (to turn on the logcat printout) $ wwlogcat (see logcat with grep filter on) $ + * statsd_testdrive (see how ww is writing the proto to statsd buffer) + */ +class StatsLogCompatManager private constructor(context: Context) : StatsLogManager(context) { + /** + * This class is purely used to support dagger bindings to be overridden in launcher variants. + * Very similar to [dagger.assisted.AssistedFactory]. But [dagger.assisted.AssistedFactory] + * cannot be overridden and this makes dagger binding difficult. + */ + class StatsLogCompatManagerFactory @Inject internal constructor() : StatsLogManagerFactory() { + override fun create(context: Context): StatsLogManager { + return StatsLogCompatManager(context) + } + } + + override fun createLogger(): StatsLogger { + return StatsCompatLogger(mContext, mActivityContext) + } + + override fun createLatencyLogger(): StatsLatencyLogger { + return StatsCompatLatencyLogger() + } + + override fun createImpressionLogger(): StatsImpressionLogger { + return StatsCompatImpressionLogger() + } + + /** Helps to construct and write statsd compatible log message. */ + private class StatsCompatLogger( + private val context: Context, + private val activityContext: ActivityContext?, + ) : StatsLogger { + + private var mItemInfo: ItemInfo? = DEFAULT_ITEM_INFO + private var mInstanceId: InstanceId? = DEFAULT_INSTANCE_ID + private var mRank: Int? = null + private var mContainerInfo: ContainerInfo? = null + private var mSrcState = LAUNCHER_STATE_UNSPECIFIED + private var mDstState = LAUNCHER_STATE_UNSPECIFIED + private var mFromState: FromState? = null + private var mToState: ToState? = null + private var mEditText: String? = null + private var mSliceItem: SliceItem? = null + private var mSlice: Slice? = null + private var mCardinality: Int? = null + private var mInputType = SysUiStatsLog.LAUNCHER_UICHANGED__INPUT_TYPE__UNKNOWN + private var mFeatures: Int? = null + private var mPackageName: String? = null + + /** Indicates the current rotation of the display. Uses [values.][android.view.Surface] */ + private val mDisplayRotation = DisplayController.INSTANCE[context].info.rotation + + override fun withItemInfo(itemInfo: ItemInfo?) = apply { + require(mContainerInfo == null) { + "ItemInfo and ContainerInfo are mutual exclusive; cannot log both." + } + mItemInfo = itemInfo + } + + override fun withInstanceId(instanceId: InstanceId?) = apply { mInstanceId = instanceId } + + override fun withRank(rank: Int) = apply { mRank = rank } + + override fun withSrcState(srcState: Int) = apply { mSrcState = srcState } + + override fun withDstState(dstState: Int) = apply { mDstState = dstState } + + override fun withContainerInfo(containerInfo: ContainerInfo?) = apply { + require(mItemInfo === DEFAULT_ITEM_INFO) { + "ItemInfo and ContainerInfo are mutual exclusive; cannot log both." + } + this.mContainerInfo = containerInfo + } + + override fun withFromState(fromState: FromState?) = apply { this.mFromState = fromState } + + override fun withToState(toState: ToState?) = apply { this.mToState = toState } + + override fun withEditText(editText: String?) = apply { this.mEditText = editText } + + override fun withSliceItem(sliceItem: SliceItem) = apply { + require(mItemInfo === DEFAULT_ITEM_INFO && mSlice == null) { + "ItemInfo, Slice and SliceItem are mutual exclusive; cannot set more than one of them." + } + this.mSliceItem = sliceItem + } + + override fun withSlice(slice: Slice) = apply { + require(mItemInfo === DEFAULT_ITEM_INFO && mSliceItem == null) { + "ItemInfo, Slice and SliceItem are mutual exclusive; cannot set more than one of them." + } + this.mSlice = slice + } + + override fun withCardinality(cardinality: Int) = apply { this.mCardinality = cardinality } + + override fun withInputType(inputType: Int) = apply { this.mInputType = inputType } + + override fun withFeatures(feature: Int) = apply { this.mFeatures = feature } + + override fun withPackageName(packageName: String?) = apply { mPackageName = packageName } + + override fun log(event: EventEnum) { + if (DEBUG) { + val name = if (event is Enum<*>) event.name else event.id.toString() + "" + Log.d(TAG, name) + } + + if (mSlice == null && mSliceItem != null) + mSlice = Slice.newBuilder().setUri(mSliceItem!!.slice!!.uri.toString()).build() + + if (mSlice != null) { + Executors.MODEL_EXECUTOR.execute { + val itemInfoBuilder = LauncherAtom.ItemInfo.newBuilder().setSlice(mSlice) + mContainerInfo?.let { itemInfoBuilder.setContainerInfo(it) } + write(event, applyOverwrites(itemInfoBuilder.build())) + } + return + } + + val info = mItemInfo ?: return + + // If the item is inside a collection, fetch collection info in a BG thread + // and then write to StatsLog. + LauncherAppState.INSTANCE[context].model.enqueueModelUpdateTask { _, dataModel, _ -> + write( + event, + applyOverwrites( + info.buildProto( + dataModel.itemsIdMap[info.container] as CollectionInfo?, + context, + ) + ), + ) + } + } + + override fun sendToInteractionJankMonitor(event: EventEnum?, view: View?) { + if (event !is LauncherEvent) return + when (event) { + LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN -> + InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_ALL_APPS_SCROLL) + + LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END -> + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_ALL_APPS_SCROLL) + LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN -> + InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_LOCK) + + LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END -> + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_LOCK) + LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN -> + InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK) + + LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END -> + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK) + LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_BEGIN -> + InteractionJankMonitorWrapper.begin( + view, + Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND, + ) + + LAUNCHER_WORK_UTILITY_VIEW_EXPAND_ANIMATION_END -> + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND) + + LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_BEGIN -> + InteractionJankMonitorWrapper.begin( + view, + Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK, + ) + + LAUNCHER_WORK_UTILITY_VIEW_SHRINK_ANIMATION_END -> + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK) + + else -> {} + } + } + + fun applyOverwrites(atomInfo: LauncherAtom.ItemInfo): LauncherAtom.ItemInfo = + atomInfo + .toBuilder() + .apply { + mRank?.let { setRank(it) } + mContainerInfo?.let { setContainerInfo(it) } + activityContext?.applyOverwritesToLogItem(this) + + if (mFromState != null || mToState != null || mEditText != null) { + val folderIconBuilder = folderIcon.toBuilder() + mFromState?.let { folderIconBuilder.setFromLabelState(it) } + mToState?.let { folderIconBuilder.setToLabelState(it) } + mEditText?.let { folderIconBuilder.setLabelInfo(it) } + + setFolderIcon(folderIconBuilder) + } + } + .build() + + @WorkerThread + fun write(event: EventEnum, atomInfo: LauncherAtom.ItemInfo) { + val instanceId = mInstanceId + val srcState = mSrcState + val dstState = mDstState + val inputType = mInputType + val packageName = mPackageName ?: getPackageName(atomInfo) + + if (IS_VERBOSE) { + val name = + if (event is Enum<*>) (event as Enum<*>).name else event.id.toString() + "" + val logStringBuilder = StringBuilder("\n") + if (instanceId !== DEFAULT_INSTANCE_ID) + logStringBuilder.append("InstanceId:$instanceId") + + logStringBuilder.append(name) + + if ( + srcState != LAUNCHER_STATE_UNSPECIFIED || dstState != LAUNCHER_STATE_UNSPECIFIED + ) { + logStringBuilder.append( + "(State:${getStateString(srcState)}->${getStateString(dstState)})" + ) + } + + if (atomInfo.hasContainerInfo()) logStringBuilder.append("\n$atomInfo") + packageName?.let { logStringBuilder.append("\nPackage name: $it") } + + Log.d(TAG, logStringBuilder.toString()) + } + + for (consumer in LOGS_CONSUMER) { + consumer.consume(event, atomInfo) + } + + // TODO: remove this when b/231648228 is fixed. + if (Utilities.isRunningInTestHarness()) { + return + } + val cardinality = mCardinality ?: getCardinality(atomInfo) + + val features = mFeatures ?: getFeatures(atomInfo) + + SysUiStatsLog.write( + SysUiStatsLog.LAUNCHER_EVENT, + SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION, /* deprecated */ + srcState, + dstState, + null, /* launcher extensions, deprecated */ + false, /* quickstep_enabled, deprecated */ + event.id, /* event_id */ + atomInfo.itemCase.number, /* target_id */ + instanceId!!.id, /* instance_id TODO */ + 0, /* uid TODO */ + packageName, /* package_name */ + getComponentName(atomInfo), /* component_name */ + getGridX(atomInfo, false), /* grid_x */ + getGridY(atomInfo, false), /* grid_y */ + getPageId(atomInfo), /* page_id */ + getGridX(atomInfo, true), /* grid_x_parent */ + getGridY(atomInfo, true), /* grid_y_parent */ + getParentPageId(atomInfo), /* page_id_parent */ + getHierarchy(atomInfo), /* hierarchy */ + false, /* is_work_profile, deprecated */ + atomInfo.rank, /* rank */ + atomInfo.folderIcon.fromLabelState.number, /* fromState */ + atomInfo.folderIcon.toLabelState.number, /* toState */ + atomInfo.folderIcon.labelInfo, /* edittext */ + cardinality, /* cardinality */ + features, /* features */ + getSearchAttributes(atomInfo), /* searchAttributes */ + getAttributes(atomInfo), /* attributes */ + inputType, /* input_type */ + atomInfo.userType, /* user_type */ + displayRotation, /* display_rotation */ + getRecentsOrientationHandler(atomInfo), /* recents_orientation_handler */ + ) + } + + val displayRotation: Int + get() = + when (mDisplayRotation) { + Surface.ROTATION_90 -> + SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_90 + Surface.ROTATION_180 -> + SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_180 + Surface.ROTATION_270 -> + SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_270 + else -> SysUiStatsLog.LAUNCHER_UICHANGED__DISPLAY_ROTATION__ROTATION_0 + } + + fun getRecentsOrientationHandler(itemInfo: LauncherAtom.ItemInfo): Int { + val orientationHandler = itemInfo.containerInfo.taskSwitcherContainer.orientationHandler + return when (orientationHandler) { + PORTRAIT -> SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__PORTRAIT + LANDSCAPE -> + SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__LANDSCAPE + SEASCAPE -> SysUiStatsLog.LAUNCHER_UICHANGED__RECENTS_ORIENTATION_HANDLER__SEASCAPE + } + } + + companion object { + private val DEFAULT_ITEM_INFO = ItemInfo() + + init { + DEFAULT_ITEM_INFO.itemType = Favorites.ITEM_TYPE_NON_ACTIONABLE + } + } + } + + /** Helps to construct and log statsd compatible latency events. */ + private class StatsCompatLatencyLogger : StatsLatencyLogger { + private var mInstanceId: InstanceId? = DEFAULT_INSTANCE_ID + private var mType: LatencyType? = UNKNOWN + private var mPackageId = 0 + private var mLatencyInMillis: Long = 0 + private var mQueryLength = -1 + private var mSubEventType = 0 + private var mCardinality = -1 + + override fun withInstanceId(instanceId: InstanceId?) = apply { + this.mInstanceId = instanceId + } + + override fun withType(type: LatencyType?) = apply { this.mType = type } + + override fun withPackageId(packageId: Int) = apply { this.mPackageId = packageId } + + override fun withLatency(latencyInMillis: Long) = apply { + this.mLatencyInMillis = latencyInMillis + } + + override fun withQueryLength(queryLength: Int) = apply { this.mQueryLength = queryLength } + + override fun withSubEventType(type: Int) = apply { this.mSubEventType = type } + + override fun withCardinality(cardinality: Int) = apply { this.mCardinality = cardinality } + + override fun log(event: EventEnum) { + if (IS_VERBOSE) { + val name = + if (event is Enum<*>) (event as Enum<*>).name else event.id.toString() + "" + Log.d(LATENCY_TAG, "InstanceId=$mInstanceId $name=${mLatencyInMillis}ms") + } + + SysUiStatsLog.write( + SysUiStatsLog.LAUNCHER_LATENCY, + event.id, // event_id + mInstanceId!!.id, // instance_id + mPackageId, // package_id + mLatencyInMillis, // latency_in_millis + mType!!.id, // type + mQueryLength, // query_length + mSubEventType, // sub_event_type + mCardinality, // cardinality + ) + } + } + + /** Helps to construct and log statsd compatible impression events. */ + private class StatsCompatImpressionLogger : StatsImpressionLogger { + private var mInstanceId: InstanceId? = DEFAULT_INSTANCE_ID + private var mLauncherState: State? = State.UNKNOWN + private var mQueryLength = -1 + + // Fields used for Impression Logging V2. + private var mResultType = 0 + private var mAboveKeyboard = false + private var mUid = 0 + private var mResultSource = 0 + + override fun withInstanceId(instanceId: InstanceId?) = apply { + this.mInstanceId = instanceId + } + + override fun withState(state: State?) = apply { this.mLauncherState = state } + + override fun withQueryLength(queryLength: Int) = apply { this.mQueryLength = queryLength } + + override fun withResultType(resultType: Int) = apply { mResultType = resultType } + + override fun withAboveKeyboard(aboveKeyboard: Boolean) = apply { + mAboveKeyboard = aboveKeyboard + } + + override fun withUid(uid: Int) = apply { mUid = uid } + + override fun withResultSource(resultSource: Int) = apply { mResultSource = resultSource } + + override fun log(event: EventEnum) { + if (IS_VERBOSE) { + val name = + if (event is Enum<*>) (event as Enum<*>).name else event.id.toString() + "" + Log.d( + IMPRESSION_TAG, + """" + |InstanceId:$mInstanceId + |ImpressionEvent:$name + | LauncherState = $mLauncherState + | QueryLength = $mQueryLength + | ResultType=$mResultType is_above_keyboard=$mAboveKeyboard mUid=$mUid + | result_source=$mResultSource + |""" + .trimMargin(), + ) + } + + SysUiStatsLog.write( + SysUiStatsLog.LAUNCHER_IMPRESSION_EVENT_V2, + event.id, // event_id + mInstanceId!!.id, // instance_id + mLauncherState!!.launcherState, // state + mQueryLength, // query_length + mResultType, // result type + mAboveKeyboard, // above keyboard + mUid, // uid + mResultSource, // result source + ) + } + } + + /** Interface to get stats log while it is dispatched to the system */ + interface StatsLogConsumer { + @WorkerThread fun consume(event: EventEnum?, atomInfo: LauncherAtom.ItemInfo?) + } + + companion object { + private const val TAG = "StatsLog" + private const val LATENCY_TAG = "StatsLatencyLog" + private const val IMPRESSION_TAG = "StatsImpressionLog" + private val IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG) + private val DEBUG = !Utilities.isRunningInTestHarness() + private val DEFAULT_INSTANCE_ID: InstanceId = InstanceId.fakeInstanceId(0) + + // LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher proto + // migrates + // from nano to lite, bake constant to prevent robo test failure. + private const val DEFAULT_PAGE_INDEX = -2 + private const val FOLDER_HIERARCHY_OFFSET = 100 + private const val SEARCH_RESULT_HIERARCHY_OFFSET = 200 + private const val EXTENDED_CONTAINERS_HIERARCHY_OFFSET = 300 + private const val ALL_APPS_HIERARCHY_OFFSET = 400 + + /** Flags for converting SearchAttribute to integer value. */ + private const val SEARCH_ATTRIBUTES_CORRECTED_QUERY = 1 shl 0 + private const val SEARCH_ATTRIBUTES_DIRECT_MATCH = 1 shl 1 + private const val SEARCH_ATTRIBUTES_ENTRY_STATE_ALL_APPS = 1 shl 2 + private const val SEARCH_ATTRIBUTES_ENTRY_STATE_QSB = 1 shl 3 + private const val SEARCH_ATTRIBUTES_ENTRY_STATE_OVERVIEW = 1 shl 4 + private const val SEARCH_ATTRIBUTES_ENTRY_STATE_TASKBAR = 1 shl 5 + + @JvmField val LOGS_CONSUMER: CopyOnWriteArrayList = CopyOnWriteArrayList() + + /** Synchronously writes an itemInfo to stats log */ + @WorkerThread + @JvmStatic + fun writeSnapshot(info: LauncherAtom.ItemInfo, instanceId: InstanceId) { + if (IS_VERBOSE) { + Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.id, info)) + } + if (Utilities.isRunningInTestHarness()) { + return + } + SysUiStatsLog.write( + SysUiStatsLog.LAUNCHER_SNAPSHOT, + LAUNCHER_WORKSPACE_SNAPSHOT.id, /* event_id */ + info.itemCase.number, /* target_id */ + instanceId.id, /* instance_id */ + 0, /* uid */ + getPackageName(info), /* package_name */ + getComponentName(info), /* component_name */ + getGridX(info, false), /* grid_x */ + getGridY(info, false), /* grid_y */ + getPageId(info), /* page_id */ + getGridX(info, true), /* grid_x_parent */ + getGridY(info, true), /* grid_y_parent */ + getParentPageId(info), /* page_id_parent */ + getHierarchy(info), /* hierarchy */ + info.isWork, /* is_work_profile */ + 0, /* origin */ + getCardinality(info), /* cardinality */ + info.widget.spanX, + info.widget.spanY, + getFeatures(info), + getAttributes(info), /* attributes */ + ) + } + + private fun getAttributes(itemInfo: LauncherAtom.ItemInfo): ByteArray = + LauncherAttributes.newBuilder() + .apply { itemInfo.itemAttributesList.forEach { addItemAttributes(it.number) } } + .build() + .toByteArray() + + /** + * Builds [StatsEvent] from [LauncherAtom.ItemInfo]. Used for pulled atom callback + * implementation. + */ + @JvmStatic + fun buildStatsEvent(info: LauncherAtom.ItemInfo, instanceId: InstanceId?): StatsEvent { + return SysUiStatsLog.buildStatsEvent( + SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT, // atom ID, + LAUNCHER_WORKSPACE_SNAPSHOT.id, // event_id = 1; + info.itemCase.number, // item_id = 2; + instanceId?.id ?: 0, // instance_id = 3; + 0, // uid = 4 [(is_uid) = true]; + getPackageName(info), // package_name = 5; + getComponentName(info), // component_name = 6; + getGridX(info, false), // grid_x = 7 [default = -1]; + getGridY(info, false), // grid_y = 8 [default = -1]; + getPageId(info), // page_id = 9 [default = -2]; + getGridX(info, true), // grid_x_parent = 10 [default = -1]; + getGridY(info, true), // grid_y_parent = 11 [default = -1]; + getParentPageId(info), // page_id_parent = 12 [default = -2]; + getHierarchy(info), // container_id = 13; + info.isWork, // is_work_profile = 14; + 0, // attribute_id = 15; + getCardinality(info), // cardinality = 16; + info.widget.spanX, // span_x = 17 [default = 1]; + info.widget.spanY, // span_y = 18 [default = 1]; + getAttributes(info), /* attributes = 19 [(log_mode) = MODE_BYTES] */ + info.isKidsMode, /* is_kids_mode = 20 */ + ) + } + + private fun getCardinality(info: LauncherAtom.ItemInfo): Int { + if (Utilities.isRunningInTestHarness()) return 0 + + when (info.containerInfo.containerCase) { + PREDICTED_HOTSEAT_CONTAINER -> + return info.containerInfo.predictedHotseatContainer.cardinality + TASK_BAR_CONTAINER -> return info.containerInfo.taskBarContainer.cardinality + SEARCH_RESULT_CONTAINER -> + return info.containerInfo.searchResultContainer.queryLength + EXTENDED_CONTAINERS -> { + val extendedCont = info.containerInfo.extendedContainers + if (extendedCont.containerCase == DEVICE_SEARCH_RESULT_CONTAINER) { + val deviceSearchResultCont = extendedCont.deviceSearchResultContainer + return if (deviceSearchResultCont.hasQueryLength()) + deviceSearchResultCont.queryLength + else -1 + } + return when (info.itemCase) { + FOLDER_ICON -> info.folderIcon.cardinality + TASK_VIEW -> info.taskView.cardinality + else -> 0 + } + } + + else -> + return when (info.itemCase) { + FOLDER_ICON -> info.folderIcon.cardinality + TASK_VIEW -> info.taskView.cardinality + else -> 0 + } + } + } + + private fun getPackageName(info: LauncherAtom.ItemInfo): String? = + when (info.itemCase) { + APPLICATION -> info.application.packageName + SHORTCUT -> info.shortcut.shortcutName + WIDGET -> info.widget.packageName + TASK -> info.task.packageName + SEARCH_ACTION_ITEM -> info.searchActionItem.packageName + else -> null + } + + private fun getComponentName(info: LauncherAtom.ItemInfo): String? = + when (info.itemCase) { + APPLICATION -> info.application.componentName + SHORTCUT -> info.shortcut.shortcutName + WIDGET -> info.widget.componentName + TASK -> info.task.componentName + TASK_VIEW -> info.taskView.componentName + SEARCH_ACTION_ITEM -> info.searchActionItem.title + SLICE -> info.slice.uri + else -> null + } + + private fun getGridX(info: LauncherAtom.ItemInfo, parent: Boolean): Int { + val containerInfo = info.containerInfo + return if (containerInfo.containerCase == FOLDER) { + if (parent) { + containerInfo.folder.workspace.gridX + } else { + containerInfo.folder.gridX + } + } else if (containerInfo.containerCase == EXTENDED_CONTAINERS) { + containerInfo.extendedContainers.deviceSearchResultContainer.gridX + } else { + containerInfo.workspace.gridX + } + } + + private fun getGridY(info: LauncherAtom.ItemInfo, parent: Boolean): Int = + if (info.containerInfo.containerCase == FOLDER) { + if (parent) { + info.containerInfo.folder.workspace.gridY + } else { + info.containerInfo.folder.gridY + } + } else { + info.containerInfo.workspace.gridY + } + + private fun getPageId(info: LauncherAtom.ItemInfo): Int = + when (info.itemCase) { + TASK -> info.task.index + TASK_VIEW -> info.taskView.index + else -> getPageIdFromContainerInfo(info.containerInfo) + } + + private fun getPageIdFromContainerInfo(containerInfo: ContainerInfo): Int = + when (containerInfo.containerCase) { + FOLDER -> containerInfo.folder.pageIndex + HOTSEAT -> containerInfo.hotseat.index + PREDICTED_HOTSEAT_CONTAINER -> containerInfo.predictedHotseatContainer.index + TASK_BAR_CONTAINER -> containerInfo.taskBarContainer.index + else -> containerInfo.workspace.pageIndex + } + + private fun getParentPageId(info: LauncherAtom.ItemInfo): Int = + info.containerInfo.run { + when { + containerCase == FOLDER && + folder.parentContainerCase == ParentContainerCase.HOTSEAT -> + folder.hotseat.index + + containerCase == FOLDER -> folder.workspace.pageIndex + containerCase == SEARCH_RESULT_CONTAINER -> + searchResultContainer.workspace.pageIndex + + else -> workspace.pageIndex + } + } + + private fun getHierarchy(info: LauncherAtom.ItemInfo): Int { + if (Utilities.isRunningInTestHarness()) return 0 + + return info.containerInfo.run { + when (containerCase) { + FOLDER -> folder.parentContainerCase.number + FOLDER_HIERARCHY_OFFSET + SEARCH_RESULT_CONTAINER -> + searchResultContainer.parentContainerCase.number + + SEARCH_RESULT_HIERARCHY_OFFSET + EXTENDED_CONTAINERS -> + extendedContainers.containerCase.number + + EXTENDED_CONTAINERS_HIERARCHY_OFFSET + ALL_APPS_CONTAINER -> + allAppsContainer.parentContainerCase.number + ALL_APPS_HIERARCHY_OFFSET + else -> containerCase.number + } + } + } + + private fun getStateString(state: Int): String = + when (state) { + SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND -> "BACKGROUND" + SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME -> "HOME" + SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW -> "OVERVIEW" + SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__ALLAPPS -> "ALLAPPS" + else -> "INVALID" + } + + private fun getFeatures(info: LauncherAtom.ItemInfo): Int = + when (info.itemCase) { + WIDGET -> info.widget.widgetFeatures + TASK_VIEW -> info.taskView.type + else -> 0 + } + + private fun getSearchAttributes(info: LauncherAtom.ItemInfo): Int { + if (Utilities.isRunningInTestHarness()) return 0 + + val containerInfo = info.containerInfo + if ( + containerInfo.containerCase == EXTENDED_CONTAINERS && + (containerInfo.extendedContainers.containerCase == + DEVICE_SEARCH_RESULT_CONTAINER) && + containerInfo.extendedContainers.deviceSearchResultContainer + .hasSearchAttributes() + ) { + return searchAttributesToInt( + containerInfo.extendedContainers.deviceSearchResultContainer.searchAttributes + ) + } + return 0 + } + + private fun searchAttributesToInt(searchAttributes: SearchAttributes): Int { + var response = 0 + if (searchAttributes.correctedQuery) + response = response or SEARCH_ATTRIBUTES_CORRECTED_QUERY + + if (searchAttributes.directMatch) response = response or SEARCH_ATTRIBUTES_DIRECT_MATCH + + response = + response or + when (searchAttributes.entryState) { + ALL_APPS -> SEARCH_ATTRIBUTES_ENTRY_STATE_ALL_APPS + QSB -> SEARCH_ATTRIBUTES_ENTRY_STATE_QSB + OVERVIEW -> SEARCH_ATTRIBUTES_ENTRY_STATE_OVERVIEW + TASKBAR -> SEARCH_ATTRIBUTES_ENTRY_STATE_TASKBAR + else -> 0 + } + + return response + } + } +} diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt index ec04cb767d..f9452f781e 100644 --- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt @@ -37,8 +37,9 @@ import android.widget.LinearLayout import androidx.annotation.VisibleForTesting import androidx.core.util.component1 import androidx.core.util.component2 +import androidx.core.view.marginStart +import androidx.core.view.updateLayoutParams import com.android.launcher3.DeviceProfile -import com.android.launcher3.Flags import com.android.launcher3.LauncherAnimUtils import com.android.launcher3.R import com.android.launcher3.Utilities @@ -47,15 +48,16 @@ import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption import com.android.launcher3.util.SplitConfigurationOptions.StagePosition import com.android.launcher3.views.BaseDragLayer import com.android.quickstep.views.IconAppChipView +import com.android.wm.shell.shared.split.SplitBounds import kotlin.math.max open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { @@ -82,9 +84,9 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { override fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile) { // We don't need to check the "top" value here because the startRect is in the orientation // of the app, not of the fixed portrait launcher. - if (outStartRect.left > deviceProfile.heightPx) { + if (outStartRect.left > deviceProfile.deviceProperties.heightPx) { outStartRect.offsetTo(0f, outStartRect.top) - } else if (outStartRect.left < -deviceProfile.heightPx) { + } else if (outStartRect.left < -deviceProfile.deviceProperties.heightPx) { outStartRect.offsetTo(0f, outStartRect.top) } } @@ -102,7 +104,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { target: T, action: Int2DAction, primaryParam: Int, - secondaryParam: Int + secondaryParam: Int, ) = action.call(target, secondaryParam, primaryParam) override fun getPrimaryDirection(event: MotionEvent, pointerIndex: Int): Float = @@ -117,6 +119,8 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { override fun getPrimarySize(rect: RectF): Float = rect.height() + override fun getSecondarySize(rect: RectF): Float = rect.width() + override fun getStart(rect: RectF): Float = rect.top override fun getEnd(rect: RectF): Float = rect.bottom @@ -170,7 +174,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { override fun getSplitTranslationDirectionFactor( stagePosition: Int, - deviceProfile: DeviceProfile + deviceProfile: DeviceProfile, ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1 override fun getTaskMenuX( @@ -178,8 +182,13 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { thumbnailView: View, deviceProfile: DeviceProfile, taskInsetMargin: Float, - taskViewIcon: View - ): Float = thumbnailView.measuredWidth + x - taskInsetMargin + taskViewIcon: View, + ): Float = + if (enableOverviewIconMenu()) { + x + (taskViewIcon as IconAppChipView).menuToCollapsedChipGap + } else { + thumbnailView.measuredWidth + x - taskInsetMargin + } override fun getTaskMenuY( y: Float, @@ -187,8 +196,13 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { stagePosition: Int, taskMenuView: View, taskInsetMargin: Float, - taskViewIcon: View + taskViewIcon: View, ): Float { + if (enableOverviewIconMenu()) { + val marginStart = (taskViewIcon as IconAppChipView).backgroundMarginTopStart + return if (taskMenuView.isLayoutRtl) y - marginStart else y + marginStart + } + val layoutParams = taskMenuView.layoutParams as BaseDragLayer.LayoutParams var taskMenuY = y + taskInsetMargin @@ -199,13 +213,19 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { return taskMenuY } + override fun getAppChipMenuMarginX(appChipView: IconAppChipView, isRtl: Boolean): Int = + appChipView.menuToCollapsedChipGap + + override fun getAppChipMenuMarginY(appChipView: IconAppChipView, isRtl: Boolean): Int = + if (isRtl) appChipView.backgroundMarginTopStart else -appChipView.backgroundMarginTopStart + override fun getTaskMenuWidth( thumbnailView: View, deviceProfile: DeviceProfile, - @StagePosition stagePosition: Int + @StagePosition stagePosition: Int, ): Int = when { - Flags.enableOverviewIconMenu() -> + enableOverviewIconMenu() -> thumbnailView.resources.getDimensionPixelSize( R.dimen.task_thumbnail_icon_menu_expanded_width ) @@ -217,14 +237,14 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { taskInsetMargin: Float, deviceProfile: DeviceProfile, taskMenuX: Float, - taskMenuY: Float + taskMenuY: Float, ): Int = (taskMenuX - taskInsetMargin).toInt() override fun setTaskOptionsMenuLayoutOrientation( deviceProfile: DeviceProfile, taskMenuLayout: LinearLayout, dividerSpacing: Int, - dividerDrawable: ShapeDrawable + dividerDrawable: ShapeDrawable, ) { taskMenuLayout.orientation = LinearLayout.VERTICAL dividerDrawable.intrinsicHeight = dividerSpacing @@ -234,7 +254,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { override fun setLayoutParamsForTaskMenuOptionItem( lp: LinearLayout.LayoutParams, viewGroup: LinearLayout, - deviceProfile: DeviceProfile + deviceProfile: DeviceProfile, ) { // Phone fake landscape viewGroup.orientation = LinearLayout.HORIZONTAL @@ -242,49 +262,54 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { lp.height = ViewGroup.LayoutParams.WRAP_CONTENT } - override fun getDwbLayoutTranslations( + override fun updateDwbBannerLayout( + taskViewWidth: Int, + taskViewHeight: Int, + isGroupedTaskView: Boolean, + deviceProfile: DeviceProfile, + snapshotViewWidth: Int, + snapshotViewHeight: Int, + banner: View, + ) { + banner.pivotX = 0f + banner.pivotY = 0f + banner.rotation = degreesRotated + banner.updateLayoutParams { + gravity = Gravity.TOP or if (banner.isLayoutRtl) Gravity.END else Gravity.START + width = + if (isGroupedTaskView) { + snapshotViewHeight + } else { + taskViewHeight - deviceProfile.overviewProfile.taskThumbnailTopMarginPx + } + } + } + + override fun getDwbBannerTranslations( taskViewWidth: Int, taskViewHeight: Int, splitBounds: SplitBounds?, deviceProfile: DeviceProfile, thumbnailViews: Array, desiredTaskId: Int, - banner: View + banner: View, ): Pair { - val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams - val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL + val snapshotParams = thumbnailViews[0].layoutParams as ViewGroup.MarginLayoutParams val translationX = banner.height.toFloat() - - val bannerParams = banner.layoutParams as FrameLayout.LayoutParams - bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START - banner.pivotX = 0f - banner.pivotY = 0f - banner.rotation = degreesRotated - - if (splitBounds == null) { - // Single, fullscreen case - bannerParams.width = taskViewHeight - snapshotParams.topMargin - return Pair(translationX, snapshotParams.topMargin.toFloat()) - } - - // Set correct width and translations val translationY: Float - if (desiredTaskId == splitBounds.leftTopTaskId) { - bannerParams.width = thumbnailViews[0].measuredHeight + if (splitBounds == null) { translationY = snapshotParams.topMargin.toFloat() } else { - bannerParams.width = thumbnailViews[1].measuredHeight - val topLeftTaskPlusDividerPercent = - if (splitBounds.appsStackedVertically) { - splitBounds.topTaskPercent + splitBounds.dividerHeightPercent - } else { - splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent - } - translationY = - snapshotParams.topMargin + - (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent + if (desiredTaskId == splitBounds.leftTopTaskId) { + translationY = snapshotParams.topMargin.toFloat() + } else { + val topLeftTaskPlusDividerPercent = + splitBounds.leftTopTaskPercent + splitBounds.dividerPercent + translationY = + snapshotParams.topMargin + + (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent + } } - return Pair(translationX, translationY) } @@ -296,17 +321,46 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE else SingleAxisSwipeDetector.DIRECTION_POSITIVE + override fun getDownDirection(isRtl: Boolean): Int = + if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE + else SingleAxisSwipeDetector.DIRECTION_NEGATIVE + override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean = if (isRtl) displacement < 0 else displacement > 0 override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1 + + override fun getTaskDismissVerticalDirection(): Int = 1 + + override fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int = + secondaryDimension - taskThumbnailBounds.left + + override fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int = + taskThumbnailBounds.left + + override fun extendRectForPrimaryTranslation(rect: Rect, translation: Int) { + if (translation < 0) { + rect.top += translation + } else { + rect.bottom += translation + } + } + + override fun extendRectForSecondaryTranslation(rect: Rect, translation: Int) { + if (translation < 0) { + rect.left += translation + } else { + rect.right += translation + } + } + /* -------------------- */ override fun getChildBounds( child: View, childStart: Int, pageCenter: Int, - layoutChild: Boolean + layoutChild: Boolean, ): ChildBounds { val childHeight = child.measuredHeight val childWidth = child.measuredWidth @@ -327,7 +381,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, STAGE_POSITION_TOP_OR_LEFT, - STAGE_TYPE_MAIN + STAGE_TYPE_MAIN, ) ) @@ -336,19 +390,19 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { placeholderInset: Int, dp: DeviceProfile, @StagePosition stagePosition: Int, - out: Rect + out: Rect, ) { // In fake land/seascape, the placeholder always needs to go to the "top" of the device, // which is the same bounds as 0 rotation. - val width = dp.widthPx + val width = dp.deviceProperties.widthPx val insetSizeAdjustment = getPlaceholderSizeAdjustment(dp) out.set(0, 0, width, placeholderHeight + insetSizeAdjustment) out.inset(placeholderInset, 0) // Adjust the top to account for content off screen. This will help to animate the view in // with rounded corners. - val screenWidth = dp.widthPx - val screenHeight = dp.heightPx + val screenWidth = dp.deviceProperties.widthPx + val screenHeight = dp.deviceProperties.heightPx val totalHeight = (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) / screenWidth).toInt() out.top -= totalHeight - placeholderHeight @@ -363,7 +417,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { drawableWidth: Int, drawableHeight: Int, dp: DeviceProfile, - @StagePosition stagePosition: Int + @StagePosition stagePosition: Int, ) { val insetAdjustment = getPlaceholderSizeAdjustment(dp) / 2f out.x = (onScreenRectCenterX / fullscreenScaleX - 1.0f * drawableWidth / 2) @@ -382,7 +436,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { out: View, dp: DeviceProfile, splitInstructionsHeight: Int, - splitInstructionsWidth: Int + splitInstructionsWidth: Int, ) { out.pivotX = 0f out.pivotY = splitInstructionsHeight.toFloat() @@ -410,11 +464,11 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { dp: DeviceProfile, @StagePosition stagePosition: Int, out1: Rect, - out2: Rect + out2: Rect, ) { // In fake land/seascape, the window bounds are always top and bottom half - val screenHeight = dp.heightPx - val screenWidth = dp.widthPx + val screenHeight = dp.deviceProperties.heightPx + val screenWidth = dp.deviceProperties.widthPx out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize) out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight) } @@ -423,17 +477,10 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { dp: DeviceProfile, outRect: Rect, splitInfo: SplitBounds, - desiredStagePosition: Int + desiredStagePosition: Int, ) { - val topLeftTaskPercent: Float - val dividerBarPercent: Float - if (splitInfo.appsStackedVertically) { - topLeftTaskPercent = splitInfo.topTaskPercent - dividerBarPercent = splitInfo.dividerHeightPercent - } else { - topLeftTaskPercent = splitInfo.leftTaskPercent - dividerBarPercent = splitInfo.dividerWidthPercent - } + val topLeftTaskPercent = splitInfo.leftTopTaskPercent + val dividerBarPercent = splitInfo.dividerPercent if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) { outRect.bottom = outRect.top + (outRect.height() * topLeftTaskPercent).toInt() @@ -442,6 +489,10 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { } } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. Currently this state is not reachable in fake landscape. + */ override fun measureGroupedTaskViewThumbnailBounds( primarySnapshot: View, secondarySnapshot: View, @@ -449,18 +500,19 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { parentHeight: Int, splitBoundsConfig: SplitBounds, dp: DeviceProfile, - isRtl: Boolean + isRtl: Boolean, + inSplitSelection: Boolean, ) { val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams // Swap the margins that are set in TaskView#setRecentsOrientedState() - secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx + secondaryParams.topMargin = dp.overviewProfile.taskThumbnailTopMarginPx primaryParams.topMargin = 0 // Measure and layout the thumbnails bottom up, since the primary is on the visual left // (portrait bottom) and secondary is on the right (portrait top) - val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx + val spaceAboveSnapshot = dp.overviewProfile.taskThumbnailTopMarginPx val totalThumbnailHeight = parentHeight - spaceAboveSnapshot val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) @@ -470,13 +522,13 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { primarySnapshot.translationY = spaceAboveSnapshot.toFloat() primarySnapshot.measure( MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY) + MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY), ) val translationY = taskViewFirst.y + spaceAboveSnapshot + dividerBar secondarySnapshot.translationY = (translationY - spaceAboveSnapshot).toFloat() secondarySnapshot.measure( MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY) + MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY), ) } @@ -484,18 +536,13 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { dp: DeviceProfile, splitBoundsConfig: SplitBounds, parentWidth: Int, - parentHeight: Int + parentHeight: Int, ): Pair { - val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx + val spaceAboveSnapshot = dp.overviewProfile.taskThumbnailTopMarginPx val totalThumbnailHeight = parentHeight - spaceAboveSnapshot val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) - val taskPercent = - if (splitBoundsConfig.appsStackedVertically) { - splitBoundsConfig.topTaskPercent - } else { - splitBoundsConfig.leftTaskPercent - } + val taskPercent = splitBoundsConfig.leftTopTaskPercent val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt()) val secondTaskViewSize = Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar) @@ -507,7 +554,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { taskIconMargin: Int, taskIconHeight: Int, thumbnailTopMargin: Int, - isRtl: Boolean + isRtl: Boolean, ) { iconParams.gravity = if (isRtl) { @@ -523,7 +570,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { override fun setIconAppChipChildrenParams( iconParams: FrameLayout.LayoutParams, - chipChildMarginStart: Int + chipChildMarginStart: Int, ) { iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL iconParams.marginStart = chipChildMarginStart @@ -534,7 +581,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { iconAppChipView: IconAppChipView, iconMenuParams: FrameLayout.LayoutParams, iconMenuMargin: Int, - thumbnailTopMargin: Int + thumbnailTopMargin: Int, ) { val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL @@ -558,6 +605,10 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { iconAppChipView.setRotation(degreesRotated) } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. Currently this state is not reachable in fake landscape. + */ override fun setSplitIconParams( primaryIconView: View, secondaryIconView: View, @@ -568,9 +619,11 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { groupedTaskViewWidth: Int, isRtl: Boolean, deviceProfile: DeviceProfile, - splitConfig: SplitBounds + splitConfig: SplitBounds, + inSplitSelection: Boolean, + oneIconHiddenDueToSmallWidth: Boolean, ) { - val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx + val spaceAboveSnapshot = deviceProfile.overviewProfile.taskThumbnailTopMarginPx val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot val dividerBar: Int = getDividerBarSize(totalThumbnailHeight, splitConfig) @@ -580,8 +633,9 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { primarySnapshotHeight, totalThumbnailHeight, isRtl, - deviceProfile.overviewTaskMarginPx, - dividerBar + deviceProfile.overviewProfile.taskMarginPx, + dividerBar, + oneIconHiddenDueToSmallWidth, ) updateSplitIconsPosition(primaryIconView, topLeftY, isRtl) @@ -595,20 +649,20 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { override fun getSplitSelectTaskOffset( primary: FloatProperty, secondary: FloatProperty, - deviceProfile: DeviceProfile + deviceProfile: DeviceProfile, ): Pair, FloatProperty> = Pair(primary, secondary) override fun getFloatingTaskOffscreenTranslationTarget( floatingTask: View, onScreenRect: RectF, @StagePosition stagePosition: Int, - dp: DeviceProfile + dp: DeviceProfile, ): Float = floatingTask.translationY - onScreenRect.height() override fun setFloatingTaskPrimaryTranslation( floatingTask: View, translation: Float, - dp: DeviceProfile + dp: DeviceProfile, ) { floatingTask.translationY = translation } @@ -638,19 +692,29 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { isRtl: Boolean, overviewTaskMarginPx: Int, dividerSize: Int, + oneIconHiddenDueToSmallWidth: Boolean, ): SplitIconPositions { - return if (Flags.enableOverviewIconMenu()) { + return if (enableOverviewIconMenu()) { if (isRtl) { - SplitIconPositions(0, -(totalThumbnailHeight - primarySnapshotHeight)) + SplitIconPositions(-(totalThumbnailHeight - primarySnapshotHeight), 0) } else { SplitIconPositions(0, primarySnapshotHeight + dividerSize) } } else { - val topLeftY = primarySnapshotHeight + overviewTaskMarginPx - SplitIconPositions( - topLeftY = topLeftY, - bottomRightY = topLeftY + dividerSize + taskIconHeight - ) + if (oneIconHiddenDueToSmallWidth) { + // Center both icons + val centerY = + primarySnapshotHeight + + overviewTaskMarginPx + + ((taskIconHeight + dividerSize) / 2) + SplitIconPositions(topLeftY = centerY, bottomRightY = centerY) + } else { + val topLeftY = primarySnapshotHeight + overviewTaskMarginPx + SplitIconPositions( + topLeftY = topLeftY, + bottomRightY = topLeftY + dividerSize + taskIconHeight, + ) + } } } @@ -666,7 +730,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { open fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) { val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams - if (Flags.enableOverviewIconMenu()) { + if (enableOverviewIconMenu()) { val appChipView = iconView as IconAppChipView layoutParams.gravity = if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END @@ -690,11 +754,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { * @return The divider size for the group task view. */ protected fun getDividerBarSize(totalThumbnailHeight: Int, splitConfig: SplitBounds): Int { - return Math.round( - totalThumbnailHeight * - if (splitConfig.appsStackedVertically) splitConfig.dividerHeightPercent - else splitConfig.dividerWidthPercent - ) + return Math.round(totalThumbnailHeight * splitConfig.dividerPercent) } /** diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java deleted file mode 100644 index eeacee1cf6..0000000000 --- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java +++ /dev/null @@ -1,820 +0,0 @@ -/* - * 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.quickstep.orientation; - -import static android.view.Gravity.BOTTOM; -import static android.view.Gravity.CENTER_HORIZONTAL; -import static android.view.Gravity.END; -import static android.view.Gravity.START; -import static android.view.Gravity.TOP; -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; - -import static com.android.launcher3.Flags.enableOverviewIconMenu; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; -import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; - -import android.graphics.Matrix; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.ShapeDrawable; -import android.util.FloatProperty; -import android.util.Pair; -import android.view.Gravity; -import android.view.Surface; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; - -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.touch.DefaultPagedViewHandler; -import com.android.launcher3.touch.SingleAxisSwipeDetector; -import com.android.launcher3.util.SplitConfigurationOptions; -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; -import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; -import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; -import com.android.quickstep.views.IconAppChipView; - -import java.util.ArrayList; -import java.util.List; - -public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements - RecentsPagedOrientationHandler { - - private final Matrix mTmpMatrix = new Matrix(); - private final RectF mTmpRectF = new RectF(); - - @Override - public T getPrimaryValue(T x, T y) { - return x; - } - - @Override - public T getSecondaryValue(T x, T y) { - return y; - } - - @Override - public boolean isLayoutNaturalToLauncher() { - return true; - } - - @Override - public void adjustFloatingIconStartVelocity(PointF velocity) { - //no-op - } - - @Override - public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) { - if (outStartRect.left > deviceProfile.widthPx) { - outStartRect.offsetTo(0, outStartRect.top); - } else if (outStartRect.left < -deviceProfile.widthPx) { - outStartRect.offsetTo(0, outStartRect.top); - } - } - - @Override - public void setSecondary(T target, Float2DAction action, float param) { - action.call(target, 0, param); - } - - @Override - public void set(T target, Int2DAction action, int primaryParam, - int secondaryParam) { - action.call(target, primaryParam, secondaryParam); - } - - @Override - public int getPrimarySize(View view) { - return view.getWidth(); - } - - @Override - public float getPrimarySize(RectF rect) { - return rect.width(); - } - - @Override - public float getStart(RectF rect) { - return rect.left; - } - - @Override - public float getEnd(RectF rect) { - return rect.right; - } - - @Override - public void rotateInsets(@NonNull Rect insets, @NonNull Rect outInsets) { - outInsets.set(insets); - } - - @Override - public int getClearAllSidePadding(View view, boolean isRtl) { - return (isRtl ? view.getPaddingRight() : - view.getPaddingLeft()) / 2; - } - - @Override - public int getSecondaryDimension(View view) { - return view.getHeight(); - } - - @Override - public FloatProperty getPrimaryViewTranslate() { - return VIEW_TRANSLATE_X; - } - - @Override - public FloatProperty getSecondaryViewTranslate() { - return VIEW_TRANSLATE_Y; - } - - @Override - public float getDegreesRotated() { - return 0; - } - - @Override - public int getRotation() { - return Surface.ROTATION_0; - } - - @Override - public void setPrimaryScale(View view, float scale) { - view.setScaleX(scale); - } - - @Override - public void setSecondaryScale(View view, float scale) { - view.setScaleY(scale); - } - - public int getSecondaryTranslationDirectionFactor() { - return -1; - } - - @Override - public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) { - if (deviceProfile.isLeftRightSplit && stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) { - return -1; - } else { - return 1; - } - } - - @Override - public float getTaskMenuX(float x, View thumbnailView, - DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) { - if (deviceProfile.isLandscape) { - return x + taskInsetMargin - + (thumbnailView.getMeasuredWidth() - thumbnailView.getMeasuredHeight()) / 2f; - } else { - return x + taskInsetMargin; - } - } - - @Override - public float getTaskMenuY(float y, View thumbnailView, int stagePosition, - View taskMenuView, float taskInsetMargin, View taskViewIcon) { - return y + taskInsetMargin; - } - - @Override - public int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile, - @StagePosition int stagePosition) { - if (enableOverviewIconMenu()) { - return thumbnailView.getResources().getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_expanded_width); - } - int padding = thumbnailView.getResources() - .getDimensionPixelSize(R.dimen.task_menu_edge_padding); - return (deviceProfile.isLandscape && !deviceProfile.isTablet - ? thumbnailView.getMeasuredHeight() - : thumbnailView.getMeasuredWidth()) - (2 * padding); - } - - @Override - public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, - float taskMenuX, float taskMenuY) { - return (int) (deviceProfile.heightPx - deviceProfile.getInsets().top - taskMenuY - - deviceProfile.getOverviewActionsClaimedSpaceBelow()); - } - - @Override - public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, - LinearLayout taskMenuLayout, int dividerSpacing, - ShapeDrawable dividerDrawable) { - taskMenuLayout.setOrientation(LinearLayout.VERTICAL); - dividerDrawable.setIntrinsicHeight(dividerSpacing); - taskMenuLayout.setDividerDrawable(dividerDrawable); - } - - @Override - public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp, - LinearLayout viewGroup, DeviceProfile deviceProfile) { - viewGroup.setOrientation(LinearLayout.HORIZONTAL); - lp.width = LinearLayout.LayoutParams.MATCH_PARENT; - lp.height = WRAP_CONTENT; - } - - @Override - public Pair getDwbLayoutTranslations(int taskViewWidth, - int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, - View[] thumbnailViews, int desiredTaskId, View banner) { - float translationX = 0; - float translationY = 0; - FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams(); - banner.setPivotX(0); - banner.setPivotY(0); - banner.setRotation(getDegreesRotated()); - if (splitBounds == null) { - // Single, fullscreen case - bannerParams.width = MATCH_PARENT; - bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL; - return new Pair<>(translationX, translationY); - } - - bannerParams.gravity = - BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL); - - // Set correct width - if (desiredTaskId == splitBounds.leftTopTaskId) { - bannerParams.width = thumbnailViews[0].getMeasuredWidth(); - } else { - bannerParams.width = thumbnailViews[1].getMeasuredWidth(); - } - - // Set translations - if (deviceProfile.isLeftRightSplit) { - if (desiredTaskId == splitBounds.rightBottomTaskId) { - float leftTopTaskPercent = splitBounds.appsStackedVertically - ? splitBounds.topTaskPercent - : splitBounds.leftTaskPercent; - float dividerThicknessPercent = splitBounds.appsStackedVertically - ? splitBounds.dividerHeightPercent - : splitBounds.dividerWidthPercent; - translationX = ((taskViewWidth * leftTopTaskPercent) - + (taskViewWidth * dividerThicknessPercent)); - } - } else { - if (desiredTaskId == splitBounds.leftTopTaskId) { - FrameLayout.LayoutParams snapshotParams = - (FrameLayout.LayoutParams) thumbnailViews[0] - .getLayoutParams(); - float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically - ? (1f - splitBounds.topTaskPercent) - : (1f - splitBounds.leftTaskPercent); - translationY = -((taskViewHeight - snapshotParams.topMargin) - * bottomRightTaskPlusDividerPercent); - } - } - return new Pair<>(translationX, translationY); - } - - /* ---------- The following are only used by TaskViewTouchHandler. ---------- */ - - @Override - public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() { - return VERTICAL; - } - - @Override - public int getUpDirection(boolean isRtl) { - // Ignore rtl since it only affects X value displacement, Y displacement doesn't change - return SingleAxisSwipeDetector.DIRECTION_POSITIVE; - } - - @Override - public boolean isGoingUp(float displacement, boolean isRtl) { - // Ignore rtl since it only affects X value displacement, Y displacement doesn't change - return displacement < 0; - } - - @Override - public int getTaskDragDisplacementFactor(boolean isRtl) { - // Ignore rtl since it only affects X value displacement, Y displacement doesn't change - return 1; - } - - /* -------------------- */ - @Override - public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) { - return dp.heightPx - rect.bottom; - } - - @Override - public List getSplitPositionOptions(DeviceProfile dp) { - if (dp.isTablet) { - return Utilities.getSplitPositionOptions(dp); - } - - List options = new ArrayList<>(); - if (dp.isSeascape()) { - options.add(new SplitPositionOption( - R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, - STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); - } else if (dp.isLeftRightSplit) { - options.add(new SplitPositionOption( - R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, - STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); - } else { - // Only add top option - options.add(new SplitPositionOption( - R.drawable.ic_split_vertical, R.string.recent_task_option_split_screen, - STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); - } - return options; - } - - @Override - public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, - DeviceProfile dp, @StagePosition int stagePosition, Rect out) { - int screenWidth = dp.widthPx; - int screenHeight = dp.heightPx; - boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; - int insetSizeAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight); - - out.set(0, 0, screenWidth, placeholderHeight + insetSizeAdjustment); - if (!dp.isLeftRightSplit) { - // portrait, phone or tablet - spans width of screen, nothing else to do - out.inset(placeholderInset, 0); - - // Adjust the top to account for content off screen. This will help to animate the view - // in with rounded corners. - int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) - / screenWidth); - out.top -= (totalHeight - placeholderHeight); - return; - } - - // Now we rotate the portrait rect depending on what side we want pinned - - float postRotateScale = (float) screenHeight / screenWidth; - mTmpMatrix.reset(); - mTmpMatrix.postRotate(pinToRight ? 90 : 270); - mTmpMatrix.postTranslate(pinToRight ? screenWidth : 0, pinToRight ? 0 : screenWidth); - // The placeholder height stays constant after rotation, so we don't change width scale - mTmpMatrix.postScale(1, postRotateScale); - - mTmpRectF.set(out); - mTmpMatrix.mapRect(mTmpRectF); - mTmpRectF.inset(0, placeholderInset); - mTmpRectF.roundOut(out); - - // Adjust the top to account for content off screen. This will help to animate the view in - // with rounded corners. - int totalWidth = (int) (1.0f * screenWidth / 2 * (screenHeight - 2 * placeholderInset) - / screenHeight); - int width = out.width(); - if (pinToRight) { - out.right += totalWidth - width; - } else { - out.left -= totalWidth - width; - } - } - - @Override - public void updateSplitIconParams(View out, float onScreenRectCenterX, - float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, - int drawableWidth, int drawableHeight, DeviceProfile dp, - @StagePosition int stagePosition) { - boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; - float insetAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight) / 2f; - if (!dp.isLeftRightSplit) { - out.setX(onScreenRectCenterX / fullscreenScaleX - - 1.0f * drawableWidth / 2); - out.setY((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY - - 1.0f * drawableHeight / 2); - } else { - if (pinToRight) { - out.setX((onScreenRectCenterX - insetAdjustment) / fullscreenScaleX - - 1.0f * drawableWidth / 2); - } else { - out.setX((onScreenRectCenterX + insetAdjustment) / fullscreenScaleX - - 1.0f * drawableWidth / 2); - } - out.setY(onScreenRectCenterY / fullscreenScaleY - - 1.0f * drawableHeight / 2); - } - } - - /** - * The split placeholder comes with a default inset to buffer the icon from the top of the - * screen. But if the device already has a large inset (from cutouts etc), use that instead. - */ - private int getPlaceholderSizeAdjustment(DeviceProfile dp, boolean pinToRight) { - int insetThickness; - if (!dp.isLandscape) { - insetThickness = dp.getInsets().top; - } else { - insetThickness = pinToRight ? dp.getInsets().right : dp.getInsets().left; - } - return Math.max(insetThickness - dp.splitPlaceholderInset, 0); - } - - @Override - public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, - int splitInstructionsWidth) { - out.setPivotX(0); - out.setPivotY(splitInstructionsHeight); - out.setRotation(getDegreesRotated()); - int distanceToEdge; - if (dp.isPhone) { - if (dp.isLandscape) { - distanceToEdge = out.getResources().getDimensionPixelSize( - R.dimen.split_instructions_bottom_margin_phone_landscape); - } else { - distanceToEdge = out.getResources().getDimensionPixelSize( - R.dimen.split_instructions_bottom_margin_phone_portrait); - } - } else { - distanceToEdge = dp.getOverviewActionsClaimedSpaceBelow(); - } - - // Center the view in case of unbalanced insets on left or right of screen - int insetCorrectionX = (dp.getInsets().right - dp.getInsets().left) / 2; - // Adjust for any insets on the bottom edge - int insetCorrectionY = dp.getInsets().bottom; - out.setTranslationX(insetCorrectionX); - out.setTranslationY(-distanceToEdge + insetCorrectionY); - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams(); - lp.gravity = CENTER_HORIZONTAL | BOTTOM; - out.setLayoutParams(lp); - } - - @Override - public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp, - @StagePosition int stagePosition, Rect out1, Rect out2) { - int screenHeight = dp.heightPx; - int screenWidth = dp.widthPx; - out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize); - out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight); - if (!dp.isLeftRightSplit) { - // Portrait - the window bounds are always top and bottom half - return; - } - - // Now we rotate the portrait rect depending on what side we want pinned - boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; - float postRotateScale = (float) screenHeight / screenWidth; - - mTmpMatrix.reset(); - mTmpMatrix.postRotate(pinToRight ? 90 : 270); - mTmpMatrix.postTranslate(pinToRight ? screenHeight : 0, pinToRight ? 0 : screenWidth); - mTmpMatrix.postScale(1 / postRotateScale, postRotateScale); - - mTmpRectF.set(out1); - mTmpMatrix.mapRect(mTmpRectF); - mTmpRectF.roundOut(out1); - - mTmpRectF.set(out2); - mTmpMatrix.mapRect(mTmpRectF); - mTmpRectF.roundOut(out2); - } - - @Override - public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, - SplitBounds splitInfo, int desiredStagePosition) { - float topLeftTaskPercent = splitInfo.appsStackedVertically - ? splitInfo.topTaskPercent - : splitInfo.leftTaskPercent; - float dividerBarPercent = splitInfo.appsStackedVertically - ? splitInfo.dividerHeightPercent - : splitInfo.dividerWidthPercent; - - int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight; - float scale = (float) outRect.height() / (dp.availableHeightPx - taskbarHeight); - float topTaskHeight = dp.availableHeightPx * topLeftTaskPercent; - float scaledTopTaskHeight = topTaskHeight * scale; - float dividerHeight = dp.availableHeightPx * dividerBarPercent; - float scaledDividerHeight = dividerHeight * scale; - - if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) { - if (dp.isLeftRightSplit) { - outRect.right = outRect.left + Math.round(outRect.width() * topLeftTaskPercent); - } else { - outRect.bottom = Math.round(outRect.top + scaledTopTaskHeight); - } - } else { - if (dp.isLeftRightSplit) { - outRect.left += Math.round(outRect.width() - * (topLeftTaskPercent + dividerBarPercent)); - } else { - outRect.top += Math.round(scaledTopTaskHeight + scaledDividerHeight); - } - } - } - - @Override - public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, - int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, - DeviceProfile dp, boolean isRtl) { - int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; - int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; - float dividerScale = splitBoundsConfig.appsStackedVertically - ? splitBoundsConfig.dividerHeightPercent - : splitBoundsConfig.dividerWidthPercent; - Pair taskViewSizes = - getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight); - if (dp.isLeftRightSplit) { - int scaledDividerBar = Math.round(parentWidth * dividerScale); - if (isRtl) { - int translationX = taskViewSizes.second.x + scaledDividerBar; - primarySnapshot.setTranslationX(-translationX); - secondarySnapshot.setTranslationX(0); - } else { - int translationX = taskViewSizes.first.x + scaledDividerBar; - secondarySnapshot.setTranslationX(translationX); - primarySnapshot.setTranslationX(0); - } - secondarySnapshot.setTranslationY(spaceAboveSnapshot); - - // Reset unused translations - primarySnapshot.setTranslationY(0); - } else { - float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale); - float translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight; - secondarySnapshot.setTranslationY(translationY); - - FrameLayout.LayoutParams primaryParams = - (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams(); - FrameLayout.LayoutParams secondaryParams = - (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams(); - secondaryParams.topMargin = 0; - primaryParams.topMargin = spaceAboveSnapshot; - - // Reset unused translations - primarySnapshot.setTranslationY(0); - secondarySnapshot.setTranslationX(0); - primarySnapshot.setTranslationX(0); - } - primarySnapshot.measure( - View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)); - secondarySnapshot.measure( - View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, - View.MeasureSpec.EXACTLY)); - primarySnapshot.setScaleX(1); - secondarySnapshot.setScaleX(1); - primarySnapshot.setScaleY(1); - secondarySnapshot.setScaleY(1); - } - - @Override - public Pair getGroupedTaskViewSizes( - DeviceProfile dp, - SplitBounds splitBoundsConfig, - int parentWidth, - int parentHeight) { - int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; - int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; - float dividerScale = splitBoundsConfig.appsStackedVertically - ? splitBoundsConfig.dividerHeightPercent - : splitBoundsConfig.dividerWidthPercent; - float taskPercent = splitBoundsConfig.appsStackedVertically - ? splitBoundsConfig.topTaskPercent - : splitBoundsConfig.leftTaskPercent; - - Point firstTaskViewSize = new Point(); - Point secondTaskViewSize = new Point(); - - if (dp.isLeftRightSplit) { - int scaledDividerBar = Math.round(parentWidth * dividerScale); - firstTaskViewSize.x = Math.round(parentWidth * taskPercent); - firstTaskViewSize.y = totalThumbnailHeight; - - secondTaskViewSize.x = parentWidth - firstTaskViewSize.x - scaledDividerBar; - secondTaskViewSize.y = totalThumbnailHeight; - } else { - int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight; - float scale = (float) totalThumbnailHeight / (dp.availableHeightPx - taskbarHeight); - float topTaskHeight = dp.availableHeightPx * taskPercent; - float finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale); - float scaledTopTaskHeight = topTaskHeight * scale; - firstTaskViewSize.x = parentWidth; - firstTaskViewSize.y = Math.round(scaledTopTaskHeight); - - secondTaskViewSize.x = parentWidth; - secondTaskViewSize.y = Math.round(totalThumbnailHeight - firstTaskViewSize.y - - finalDividerHeight); - } - - return new Pair<>(firstTaskViewSize, secondTaskViewSize); - } - - @Override - public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, - int taskIconHeight, int thumbnailTopMargin, boolean isRtl) { - iconParams.gravity = TOP | CENTER_HORIZONTAL; - // Reset margins, since they may have been set on rotation - iconParams.leftMargin = iconParams.rightMargin = 0; - iconParams.topMargin = iconParams.bottomMargin = 0; - } - - @Override - public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams, - int chipChildMarginStart) { - iconParams.setMarginStart(chipChildMarginStart); - iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL; - iconParams.topMargin = 0; - } - - @Override - public void setIconAppChipMenuParams(IconAppChipView iconAppChipView, - FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) { - iconMenuParams.gravity = TOP | START; - iconMenuParams.setMarginStart(iconMenuMargin); - iconMenuParams.topMargin = thumbnailTopMargin; - iconMenuParams.bottomMargin = 0; - iconMenuParams.setMarginEnd(0); - - iconAppChipView.setPivotX(0); - iconAppChipView.setPivotY(0); - iconAppChipView.setSplitTranslationY(0); - iconAppChipView.setRotation(getDegreesRotated()); - } - - @Override - public void setSplitIconParams(View primaryIconView, View secondaryIconView, - int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, - int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, - DeviceProfile deviceProfile, SplitBounds splitConfig) { - FrameLayout.LayoutParams primaryIconParams = - (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); - FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu() - ? (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams() - : new FrameLayout.LayoutParams(primaryIconParams); - - if (enableOverviewIconMenu()) { - IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView; - IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView; - primaryIconParams.gravity = TOP | START; - secondaryIconParams.gravity = TOP | START; - secondaryIconParams.topMargin = primaryIconParams.topMargin; - secondaryIconParams.setMarginStart(primaryIconParams.getMarginStart()); - if (deviceProfile.isLeftRightSplit) { - if (isRtl) { - int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth; - primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth); - } else { - secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth); - } - } else { - primaryAppChipView.setSplitTranslationX(0); - secondaryAppChipView.setSplitTranslationX(0); - int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(), - splitConfig.visualDividerBounds.height()); - secondaryAppChipView.setSplitTranslationY( - primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness)); - } - } else if (deviceProfile.isLeftRightSplit) { - // We calculate the "midpoint" of the thumbnail area, and place the icons there. - // This is the place where the thumbnail area splits by default, in a near-50/50 split. - // It is usually not exactly 50/50, due to insets/screen cutouts. - int fullscreenInsetThickness = deviceProfile.isSeascape() - ? deviceProfile.getInsets().right - : deviceProfile.getInsets().left; - int fullscreenMidpointFromBottom = ((deviceProfile.widthPx - - fullscreenInsetThickness) / 2); - float midpointFromEndPct = (float) fullscreenMidpointFromBottom - / deviceProfile.widthPx; - float insetPct = (float) fullscreenInsetThickness / deviceProfile.widthPx; - int spaceAboveSnapshots = 0; - int overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots; - int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness - * midpointFromEndPct); - int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); - - if (deviceProfile.isSeascape()) { - primaryIconParams.gravity = TOP | (isRtl ? END : START); - secondaryIconParams.gravity = TOP | (isRtl ? END : START); - if (splitConfig.initiatedFromSeascape) { - // if the split was initiated from seascape, - // the task on the right (secondary) is slightly larger - primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight); - secondaryIconView.setTranslationX(bottomToMidpointOffset); - } else { - // if not, - // the task on the left (primary) is slightly larger - primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset - - taskIconHeight); - secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset); - } - } else { - primaryIconParams.gravity = TOP | (isRtl ? START : END); - secondaryIconParams.gravity = TOP | (isRtl ? START : END); - if (!splitConfig.initiatedFromSeascape) { - // if the split was initiated from landscape, - // the task on the left (primary) is slightly larger - primaryIconView.setTranslationX(-bottomToMidpointOffset); - secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight); - } else { - // if not, - // the task on the right (secondary) is slightly larger - primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset); - secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset - + taskIconHeight); - } - } - } else { - primaryIconParams.gravity = TOP | CENTER_HORIZONTAL; - // shifts icon half a width left (height is used here since icons are square) - primaryIconView.setTranslationX(-(taskIconHeight / 2f)); - secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL; - secondaryIconView.setTranslationX(taskIconHeight / 2f); - } - if (!enableOverviewIconMenu()) { - primaryIconView.setTranslationY(0); - secondaryIconView.setTranslationY(0); - } - - primaryIconView.setLayoutParams(primaryIconParams); - secondaryIconView.setLayoutParams(secondaryIconParams); - } - - @Override - public int getDefaultSplitPosition(DeviceProfile deviceProfile) { - if (!deviceProfile.isTablet) { - throw new IllegalStateException("Default position available only for large screens"); - } - if (deviceProfile.isLeftRightSplit) { - return STAGE_POSITION_BOTTOM_OR_RIGHT; - } else { - return STAGE_POSITION_TOP_OR_LEFT; - } - } - - @Override - public Pair getSplitSelectTaskOffset(FloatProperty primary, - FloatProperty secondary, DeviceProfile deviceProfile) { - if (deviceProfile.isLeftRightSplit) { // or seascape - return new Pair<>(primary, secondary); - } else { - return new Pair<>(secondary, primary); - } - } - - @Override - public float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect, - @StagePosition int stagePosition, DeviceProfile dp) { - if (dp.isLeftRightSplit) { - float currentTranslationX = floatingTask.getTranslationX(); - return stagePosition == STAGE_POSITION_TOP_OR_LEFT - ? currentTranslationX - onScreenRect.width() - : currentTranslationX + onScreenRect.width(); - } else { - float currentTranslationY = floatingTask.getTranslationY(); - return currentTranslationY - onScreenRect.height(); - } - } - - @Override - public void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, - DeviceProfile dp) { - if (dp.isLeftRightSplit) { - floatingTask.setTranslationX(translation); - } else { - floatingTask.setTranslationY(translation); - } - - } - - @Override - public float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) { - return dp.isLeftRightSplit - ? floatingTask.getTranslationX() - : floatingTask.getTranslationY(); - } - - @NonNull - @Override - public LauncherAtom.TaskSwitcherContainer.OrientationHandler getHandlerTypeForLogging() { - return LauncherAtom.TaskSwitcherContainer.OrientationHandler.PORTRAIT; - } -} diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt new file mode 100644 index 0000000000..a51beead06 --- /dev/null +++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt @@ -0,0 +1,962 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.orientation + +import android.graphics.Matrix +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.ShapeDrawable +import android.util.FloatProperty +import android.util.Pair +import android.view.Gravity +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.core.view.updateLayoutParams +import com.android.launcher3.DeviceProfile +import com.android.launcher3.LauncherAnimUtils +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.logger.LauncherAtom +import com.android.launcher3.touch.DefaultPagedViewHandler +import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction +import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction +import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption +import com.android.launcher3.util.SplitConfigurationOptions.StagePosition +import com.android.quickstep.views.IconAppChipView +import com.android.wm.shell.shared.split.SplitBounds +import kotlin.math.max +import kotlin.math.min + +class PortraitPagedViewHandler : DefaultPagedViewHandler(), RecentsPagedOrientationHandler { + private val tmpMatrix = Matrix() + private val tmpRectF = RectF() + + override fun getPrimaryValue(x: T, y: T): T = x + + override fun getSecondaryValue(x: T, y: T): T = y + + override val isLayoutNaturalToLauncher: Boolean = true + + override fun adjustFloatingIconStartVelocity(velocity: PointF) { + // no-op + } + + override fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile) { + if (outStartRect.left > deviceProfile.deviceProperties.widthPx) { + outStartRect.offsetTo(0f, outStartRect.top) + } else if (outStartRect.left < -deviceProfile.deviceProperties.widthPx) { + outStartRect.offsetTo(0f, outStartRect.top) + } + } + + override fun setSecondary(target: T, action: Float2DAction, param: Float) = + action.call(target, 0f, param) + + override fun set( + target: T, + action: Int2DAction, + primaryParam: Int, + secondaryParam: Int, + ) = action.call(target, primaryParam, secondaryParam) + + override fun getPrimarySize(view: View): Int = view.width + + override fun getPrimarySize(rect: RectF): Float = rect.width() + + override fun getSecondarySize(rect: RectF): Float = rect.height() + + override fun getStart(rect: RectF): Float = rect.left + + override fun getEnd(rect: RectF): Float = rect.right + + override fun rotateInsets(insets: Rect, outInsets: Rect) = outInsets.set(insets) + + override fun getClearAllSidePadding(view: View, isRtl: Boolean): Int = + (if (isRtl) view.paddingRight else -view.paddingLeft) / 2 + + override fun getSecondaryDimension(view: View): Int = view.height + + override val primaryViewTranslate: FloatProperty = LauncherAnimUtils.VIEW_TRANSLATE_X + + override val secondaryViewTranslate: FloatProperty = LauncherAnimUtils.VIEW_TRANSLATE_Y + + override val degreesRotated: Float = 0f + + override val rotation: Int = Surface.ROTATION_0 + + override fun setPrimaryScale(view: View, scale: Float) { + view.scaleX = scale + } + + override fun setSecondaryScale(view: View, scale: Float) { + view.scaleY = scale + } + + override val secondaryTranslationDirectionFactor: Int + get() = -1 + + override fun getSplitTranslationDirectionFactor( + stagePosition: Int, + deviceProfile: DeviceProfile, + ): Int = + if ( + deviceProfile.isLeftRightSplit && + stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT + ) { + -1 + } else { + 1 + } + + override fun getTaskMenuX( + x: Float, + thumbnailView: View, + deviceProfile: DeviceProfile, + taskInsetMargin: Float, + taskViewIcon: View, + ): Float = + if (enableOverviewIconMenu()) { + x + } else { + if (deviceProfile.deviceProperties.isLandscape) { + (x + + taskInsetMargin + + (thumbnailView.measuredWidth - thumbnailView.measuredHeight) / 2f) + } else { + x + taskInsetMargin + } + } + + override fun getTaskMenuY( + y: Float, + thumbnailView: View, + stagePosition: Int, + taskMenuView: View, + taskInsetMargin: Float, + taskViewIcon: View, + ): Float = + if (enableOverviewIconMenu()) { + taskViewIcon as IconAppChipView + y - taskViewIcon.menuToCollapsedChipGap + } else { + y + taskInsetMargin + } + + override fun getAppChipMenuMarginX(appChipView: IconAppChipView, isRtl: Boolean): Int = + if (isRtl) -appChipView.backgroundMarginTopStart else appChipView.backgroundMarginTopStart + + override fun getAppChipMenuMarginY(appChipView: IconAppChipView, isRtl: Boolean): Int = + appChipView.menuToCollapsedChipGap + + override fun getTaskMenuWidth( + thumbnailView: View, + deviceProfile: DeviceProfile, + @StagePosition stagePosition: Int, + ): Int = + when { + enableOverviewIconMenu() -> { + thumbnailView.resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_expanded_width + ) + } + + (deviceProfile.deviceProperties.isLandscape && + !deviceProfile.deviceProperties.isTablet) -> { + val padding = + thumbnailView.resources.getDimensionPixelSize(R.dimen.task_menu_edge_padding) + thumbnailView.measuredHeight - (2 * padding) + } + + else -> { + val padding = + thumbnailView.resources.getDimensionPixelSize(R.dimen.task_menu_edge_padding) + thumbnailView.measuredWidth - (2 * padding) + } + } + + override fun getTaskMenuHeight( + taskInsetMargin: Float, + deviceProfile: DeviceProfile, + taskMenuX: Float, + taskMenuY: Float, + ): Int = + deviceProfile.deviceProperties.heightPx - + deviceProfile.insets.top - + taskMenuY.toInt() - + deviceProfile.overviewActionsClaimedSpaceBelow + + override fun setTaskOptionsMenuLayoutOrientation( + deviceProfile: DeviceProfile, + taskMenuLayout: LinearLayout, + dividerSpacing: Int, + dividerDrawable: ShapeDrawable, + ) { + taskMenuLayout.orientation = LinearLayout.VERTICAL + dividerDrawable.intrinsicHeight = dividerSpacing + taskMenuLayout.dividerDrawable = dividerDrawable + } + + override fun setLayoutParamsForTaskMenuOptionItem( + lp: LinearLayout.LayoutParams, + viewGroup: LinearLayout, + deviceProfile: DeviceProfile, + ) { + viewGroup.orientation = LinearLayout.HORIZONTAL + lp.width = LinearLayout.LayoutParams.MATCH_PARENT + lp.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + + override fun updateDwbBannerLayout( + taskViewWidth: Int, + taskViewHeight: Int, + isGroupedTaskView: Boolean, + deviceProfile: DeviceProfile, + snapshotViewWidth: Int, + snapshotViewHeight: Int, + banner: View, + ) { + banner.pivotX = 0f + banner.pivotY = 0f + banner.rotation = degreesRotated + banner.updateLayoutParams { + if (isGroupedTaskView) { + gravity = + Gravity.BOTTOM or + (if (deviceProfile.isLeftRightSplit) Gravity.START + else Gravity.CENTER_HORIZONTAL) + width = snapshotViewWidth + } else { + width = ViewGroup.LayoutParams.MATCH_PARENT + gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + } + } + } + + override fun getDwbBannerTranslations( + taskViewWidth: Int, + taskViewHeight: Int, + splitBounds: SplitBounds?, + deviceProfile: DeviceProfile, + thumbnailViews: Array, + desiredTaskId: Int, + banner: View, + ): Pair { + var translationX = 0f + var translationY = 0f + if (splitBounds != null) { + if (deviceProfile.isLeftRightSplit) { + if (desiredTaskId == splitBounds.rightBottomTaskId) { + val leftTopTaskPercent = splitBounds.leftTopTaskPercent + val dividerThicknessPercent = splitBounds.dividerPercent + translationX = + ((taskViewWidth * leftTopTaskPercent) + + (taskViewWidth * dividerThicknessPercent)) + } + } else { + if (desiredTaskId == splitBounds.leftTopTaskId) { + val snapshotParams = + thumbnailViews[0].layoutParams as ViewGroup.MarginLayoutParams + val bottomRightTaskPlusDividerPercent = + (splitBounds.rightBottomTaskPercent + splitBounds.dividerPercent) + translationY = + -((taskViewHeight - snapshotParams.topMargin) * + bottomRightTaskPlusDividerPercent) + } + } + } + return Pair(translationX, translationY) + } + + /* ---------- The following are only used by TaskViewTouchHandler. ---------- */ + + override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction = + SingleAxisSwipeDetector.VERTICAL + + // Ignore rtl since it only affects X value displacement, Y displacement doesn't change + override fun getUpDirection(isRtl: Boolean): Int = SingleAxisSwipeDetector.DIRECTION_POSITIVE + + // Ignore rtl since it only affects X value displacement, Y displacement doesn't change + override fun getDownDirection(isRtl: Boolean): Int = SingleAxisSwipeDetector.DIRECTION_NEGATIVE + + // Ignore rtl since it only affects X value displacement, Y displacement doesn't change + override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean = displacement < 0 + + // Ignore rtl since it only affects X value displacement, Y displacement doesn't change + override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = 1 + + override fun getTaskDismissVerticalDirection(): Int = -1 + + override fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int = + taskThumbnailBounds.bottom + + override fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int = + secondaryDimension - taskThumbnailBounds.bottom + + override fun extendRectForPrimaryTranslation(rect: Rect, translation: Int) { + if (translation < 0) { + rect.left += translation + } else { + rect.right += translation + } + } + + override fun extendRectForSecondaryTranslation(rect: Rect, translation: Int) { + if (translation < 0) { + rect.top += translation + } else { + rect.bottom += translation + } + } + + /* -------------------- */ + + override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int = + dp.deviceProperties.heightPx - rect.bottom + + override fun getSplitPositionOptions(dp: DeviceProfile): List = + when { + dp.deviceProperties.isTablet -> { + Utilities.getSplitPositionOptions(dp) + } + + dp.isSeascape -> { + listOf( + SplitPositionOption( + R.drawable.ic_split_horizontal, + R.string.recent_task_option_split_screen, + SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT, + SplitConfigurationOptions.STAGE_TYPE_MAIN, + ) + ) + } + + dp.isLeftRightSplit -> { + listOf( + SplitPositionOption( + R.drawable.ic_split_horizontal, + R.string.recent_task_option_split_screen, + SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, + SplitConfigurationOptions.STAGE_TYPE_MAIN, + ) + ) + } + + else -> { + // Only add top option + listOf( + SplitPositionOption( + R.drawable.ic_split_vertical, + R.string.recent_task_option_split_screen, + SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, + SplitConfigurationOptions.STAGE_TYPE_MAIN, + ) + ) + } + } + + override fun getInitialSplitPlaceholderBounds( + placeholderHeight: Int, + placeholderInset: Int, + dp: DeviceProfile, + @StagePosition stagePosition: Int, + out: Rect, + ) { + val screenWidth = dp.deviceProperties.widthPx + val screenHeight = dp.deviceProperties.heightPx + val pinToRight = stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT + val insetSizeAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight) + + out.set(0, 0, screenWidth, placeholderHeight + insetSizeAdjustment) + if (!dp.isLeftRightSplit) { + // portrait, phone or tablet - spans width of screen, nothing else to do + out.inset(placeholderInset, 0) + + // Adjust the top to account for content off screen. This will help to animate the view + // in with rounded corners. + val totalHeight = + (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) / screenWidth) + .toInt() + out.top -= (totalHeight - placeholderHeight) + return + } + + // Now we rotate the portrait rect depending on what side we want pinned + val postRotateScale = screenHeight.toFloat() / screenWidth + tmpMatrix.reset() + tmpMatrix.postRotate(if (pinToRight) 90f else 270f) + tmpMatrix.postTranslate( + (if (pinToRight) screenWidth else 0).toFloat(), + (if (pinToRight) 0 else screenWidth).toFloat(), + ) + // The placeholder height stays constant after rotation, so we don't change width scale + tmpMatrix.postScale(1f, postRotateScale) + + tmpRectF.set(out) + tmpMatrix.mapRect(tmpRectF) + tmpRectF.inset(0f, placeholderInset.toFloat()) + tmpRectF.roundOut(out) + + // Adjust the top to account for content off screen. This will help to animate the view in + // with rounded corners. + val totalWidth = + (1.0f * screenWidth / 2 * (screenHeight - 2 * placeholderInset) / screenHeight).toInt() + val width = out.width() + if (pinToRight) { + out.right += totalWidth - width + } else { + out.left -= totalWidth - width + } + } + + override fun updateSplitIconParams( + out: View, + onScreenRectCenterX: Float, + onScreenRectCenterY: Float, + fullscreenScaleX: Float, + fullscreenScaleY: Float, + drawableWidth: Int, + drawableHeight: Int, + dp: DeviceProfile, + @StagePosition stagePosition: Int, + ) { + val pinToRight = stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT + val insetAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight) / 2f + if (!dp.isLeftRightSplit) { + out.x = (onScreenRectCenterX / fullscreenScaleX - 1.0f * drawableWidth / 2) + out.y = + ((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY - + 1.0f * drawableHeight / 2) + } else { + if (pinToRight) { + out.x = + ((onScreenRectCenterX - insetAdjustment) / fullscreenScaleX - + 1.0f * drawableWidth / 2) + } else { + out.x = + ((onScreenRectCenterX + insetAdjustment) / fullscreenScaleX - + 1.0f * drawableWidth / 2) + } + out.y = (onScreenRectCenterY / fullscreenScaleY - 1.0f * drawableHeight / 2) + } + } + + /** + * The split placeholder comes with a default inset to buffer the icon from the top of the + * screen. But if the device already has a large inset (from cutouts etc), use that instead. + */ + private fun getPlaceholderSizeAdjustment(dp: DeviceProfile, pinToRight: Boolean): Int { + val insetThickness = + if (!dp.deviceProperties.isLandscape) { + dp.insets.top + } else { + if (pinToRight) dp.insets.right else dp.insets.left + } + return max((insetThickness - dp.splitPlaceholderInset).toDouble(), 0.0).toInt() + } + + override fun setSplitInstructionsParams( + out: View, + dp: DeviceProfile, + splitInstructionsHeight: Int, + splitInstructionsWidth: Int, + ) { + out.pivotX = 0f + out.pivotY = splitInstructionsHeight.toFloat() + out.rotation = degreesRotated + val distanceToEdge = + if (dp.deviceProperties.isPhone) { + if (dp.deviceProperties.isLandscape) { + out.resources.getDimensionPixelSize( + R.dimen.split_instructions_bottom_margin_phone_landscape + ) + } else { + out.resources.getDimensionPixelSize( + R.dimen.split_instructions_bottom_margin_phone_portrait + ) + } + } else { + dp.overviewActionsClaimedSpaceBelow + } + + // Center the view in case of unbalanced insets on left or right of screen + val insetCorrectionX = (dp.insets.right - dp.insets.left) / 2 + // Adjust for any insets on the bottom edge + val insetCorrectionY = dp.insets.bottom + out.translationX = insetCorrectionX.toFloat() + out.translationY = (-distanceToEdge + insetCorrectionY).toFloat() + val lp = out.layoutParams as FrameLayout.LayoutParams + lp.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM + out.layoutParams = lp + } + + override fun getFinalSplitPlaceholderBounds( + splitDividerSize: Int, + dp: DeviceProfile, + @StagePosition stagePosition: Int, + out1: Rect, + out2: Rect, + ) { + val screenHeight = dp.deviceProperties.heightPx + val screenWidth = dp.deviceProperties.widthPx + out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize) + out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight) + if (!dp.isLeftRightSplit) { + // Portrait - the window bounds are always top and bottom half + return + } + + // Now we rotate the portrait rect depending on what side we want pinned + val pinToRight = stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT + val postRotateScale = screenHeight.toFloat() / screenWidth + + tmpMatrix.reset() + tmpMatrix.postRotate(if (pinToRight) 90f else 270f) + tmpMatrix.postTranslate( + (if (pinToRight) screenHeight else 0).toFloat(), + (if (pinToRight) 0 else screenWidth).toFloat(), + ) + tmpMatrix.postScale(1 / postRotateScale, postRotateScale) + + tmpRectF.set(out1) + tmpMatrix.mapRect(tmpRectF) + tmpRectF.roundOut(out1) + + tmpRectF.set(out2) + tmpMatrix.mapRect(tmpRectF) + tmpRectF.roundOut(out2) + } + + override fun setSplitTaskSwipeRect( + dp: DeviceProfile, + outRect: Rect, + splitInfo: SplitBounds, + desiredStagePosition: Int, + ) { + val topLeftTaskPercent = splitInfo.leftTopTaskPercent + val dividerBarPercent = splitInfo.dividerPercent + + val taskbarHeight = + if (dp.taskbarProfile.isTransientTaskbar) 0 else dp.taskbarProfile.height + val scale = + outRect.height().toFloat() / (dp.deviceProperties.availableHeightPx - taskbarHeight) + val topTaskHeight = dp.deviceProperties.availableHeightPx * topLeftTaskPercent + val scaledTopTaskHeight = topTaskHeight * scale + val dividerHeight = dp.deviceProperties.availableHeightPx * dividerBarPercent + val scaledDividerHeight = dividerHeight * scale + + if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) { + if (dp.isLeftRightSplit) { + outRect.right = outRect.left + Math.round(outRect.width() * topLeftTaskPercent) + } else { + outRect.bottom = Math.round(outRect.top + scaledTopTaskHeight) + } + } else { + if (dp.isLeftRightSplit) { + outRect.left += + Math.round(outRect.width() * (topLeftTaskPercent + dividerBarPercent)) + } else { + outRect.top += Math.round(scaledTopTaskHeight + scaledDividerHeight) + } + } + } + + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. If true, we have custom translations/scaling in place for the remaining + * snapshot, so we'll skip setting translation/scale here. + */ + override fun measureGroupedTaskViewThumbnailBounds( + primarySnapshot: View, + secondarySnapshot: View, + parentWidth: Int, + parentHeight: Int, + splitBoundsConfig: SplitBounds, + dp: DeviceProfile, + isRtl: Boolean, + inSplitSelection: Boolean, + ) { + val spaceAboveSnapshot = dp.overviewProfile.taskThumbnailTopMarginPx + + val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams + val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams + + // Reset margins that aren't used in this method, but are used in other + // `RecentsPagedOrientationHandler` variants. + secondaryParams.topMargin = 0 + primaryParams.topMargin = spaceAboveSnapshot + + val totalThumbnailHeight = parentHeight - spaceAboveSnapshot + val dividerScale = splitBoundsConfig.dividerPercent + val taskViewSizes = + getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight) + if (!inSplitSelection) { + // Reset translations that aren't used in this method, but are used in other + // `RecentsPagedOrientationHandler` variants. + primarySnapshot.translationY = 0f + + if (dp.isLeftRightSplit) { + val scaledDividerBar = Math.round(parentWidth * dividerScale) + if (isRtl) { + val translationX = taskViewSizes.second.x + scaledDividerBar + primarySnapshot.translationX = -translationX.toFloat() + secondarySnapshot.translationX = 0f + } else { + val translationX = taskViewSizes.first.x + scaledDividerBar + secondarySnapshot.translationX = translationX.toFloat() + primarySnapshot.translationX = 0f + } + secondarySnapshot.translationY = spaceAboveSnapshot.toFloat() + } else { + val finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale).toFloat() + val translationY = taskViewSizes.first.y + spaceAboveSnapshot + finalDividerHeight + secondarySnapshot.translationY = translationY + + // Reset unused translations. + secondarySnapshot.translationX = 0f + primarySnapshot.translationX = 0f + } + } + + primarySnapshot.measure( + View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY), + ) + secondarySnapshot.measure( + View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY), + ) + } + + override fun getGroupedTaskViewSizes( + dp: DeviceProfile, + splitBoundsConfig: SplitBounds, + parentWidth: Int, + parentHeight: Int, + ): Pair { + val spaceAboveSnapshot = dp.overviewProfile.taskThumbnailTopMarginPx + val totalThumbnailHeight = parentHeight - spaceAboveSnapshot + val dividerScale = splitBoundsConfig.dividerPercent + val taskPercent = splitBoundsConfig.leftTopTaskPercent + + val firstTaskViewSize = Point() + val secondTaskViewSize = Point() + + if (dp.isLeftRightSplit) { + val scaledDividerBar = Math.round(parentWidth * dividerScale) + firstTaskViewSize.x = Math.round(parentWidth * taskPercent) + firstTaskViewSize.y = totalThumbnailHeight + + secondTaskViewSize.x = parentWidth - firstTaskViewSize.x - scaledDividerBar + secondTaskViewSize.y = totalThumbnailHeight + } else { + val taskbarHeight = + if (dp.taskbarProfile.isTransientTaskbar) 0 else dp.taskbarProfile.height + val scale = + totalThumbnailHeight.toFloat() / + (dp.deviceProperties.availableHeightPx - taskbarHeight) + val topTaskHeight = dp.deviceProperties.availableHeightPx * taskPercent + val finalDividerHeight = Math.round(totalThumbnailHeight * dividerScale).toFloat() + val scaledTopTaskHeight = topTaskHeight * scale + firstTaskViewSize.x = parentWidth + firstTaskViewSize.y = Math.round(scaledTopTaskHeight) + + secondTaskViewSize.x = parentWidth + secondTaskViewSize.y = + Math.round((totalThumbnailHeight - firstTaskViewSize.y - finalDividerHeight)) + } + + return Pair(firstTaskViewSize, secondTaskViewSize) + } + + override fun setTaskIconParams( + iconParams: FrameLayout.LayoutParams, + taskIconMargin: Int, + taskIconHeight: Int, + thumbnailTopMargin: Int, + isRtl: Boolean, + ) { + iconParams.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL + // Reset margins, since they may have been set on rotation + iconParams.rightMargin = 0 + iconParams.leftMargin = iconParams.rightMargin + iconParams.bottomMargin = 0 + iconParams.topMargin = iconParams.bottomMargin + } + + override fun setIconAppChipChildrenParams( + iconParams: FrameLayout.LayoutParams, + chipChildMarginStart: Int, + ) { + iconParams.marginStart = chipChildMarginStart + iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL + iconParams.topMargin = 0 + } + + override fun setIconAppChipMenuParams( + iconAppChipView: IconAppChipView, + iconMenuParams: FrameLayout.LayoutParams, + iconMenuMargin: Int, + thumbnailTopMargin: Int, + ) { + iconMenuParams.gravity = Gravity.TOP or Gravity.START + iconMenuParams.marginStart = iconMenuMargin + iconMenuParams.topMargin = thumbnailTopMargin + iconMenuParams.bottomMargin = 0 + iconMenuParams.marginEnd = 0 + + iconAppChipView.pivotX = 0f + iconAppChipView.pivotY = 0f + iconAppChipView.setSplitTranslationY(0f) + iconAppChipView.rotation = degreesRotated + } + + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. If true, we have custom translations in place for the remaining icon, so + * we'll skip setting translations here. + */ + override fun setSplitIconParams( + primaryIconView: View, + secondaryIconView: View, + taskIconHeight: Int, + primarySnapshotWidth: Int, + primarySnapshotHeight: Int, + groupedTaskViewHeight: Int, + groupedTaskViewWidth: Int, + isRtl: Boolean, + deviceProfile: DeviceProfile, + splitConfig: SplitBounds, + inSplitSelection: Boolean, + oneIconHiddenDueToSmallWidth: Boolean, + ) { + val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams + val secondaryIconParams = + if (enableOverviewIconMenu()) secondaryIconView.layoutParams as FrameLayout.LayoutParams + else FrameLayout.LayoutParams(primaryIconParams) + + if (enableOverviewIconMenu()) { + val primaryAppChipView = primaryIconView as IconAppChipView + val secondaryAppChipView = secondaryIconView as IconAppChipView + primaryIconParams.gravity = Gravity.TOP or Gravity.START + secondaryIconParams.gravity = Gravity.TOP or Gravity.START + secondaryIconParams.topMargin = primaryIconParams.topMargin + secondaryIconParams.marginStart = primaryIconParams.marginStart + if (!inSplitSelection) { + if (deviceProfile.isLeftRightSplit) { + if (isRtl) { + val secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth + primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth.toFloat()) + } else { + val dividerSize = + Math.round(groupedTaskViewWidth * splitConfig.dividerPercent) + secondaryAppChipView.setSplitTranslationX( + primarySnapshotWidth.toFloat() + dividerSize + ) + } + } else { + primaryAppChipView.setSplitTranslationX(0f) + secondaryAppChipView.setSplitTranslationX(0f) + val dividerThickness = + min( + splitConfig.visualDividerBounds.width().toDouble(), + splitConfig.visualDividerBounds.height().toDouble(), + ) + .toInt() + secondaryAppChipView.setSplitTranslationY( + (primarySnapshotHeight + + (if (deviceProfile.deviceProperties.isTablet) 0 + else dividerThickness)) + .toFloat() + ) + } + } + } else if (deviceProfile.isLeftRightSplit) { + // We calculate the "midpoint" of the thumbnail area, and place the icons there. + // This is the place where the thumbnail area splits by default, in a near-50/50 split. + // It is usually not exactly 50/50, due to insets/screen cutouts. + val fullscreenInsetThickness = + if (deviceProfile.isSeascape) deviceProfile.insets.right + else deviceProfile.insets.left + val fullscreenMidpointFromBottom = + ((deviceProfile.deviceProperties.widthPx - fullscreenInsetThickness) / 2) + val midpointFromEndPct = + fullscreenMidpointFromBottom.toFloat() / deviceProfile.deviceProperties.widthPx + val insetPct = + fullscreenInsetThickness.toFloat() / deviceProfile.deviceProperties.widthPx + val spaceAboveSnapshots = 0 + val overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots + val bottomToMidpointOffset = + (overviewThumbnailAreaThickness * midpointFromEndPct).toInt() + val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt() + + if (deviceProfile.isSeascape) { + primaryIconParams.gravity = + Gravity.TOP or (if (isRtl) Gravity.END else Gravity.START) + secondaryIconParams.gravity = + Gravity.TOP or (if (isRtl) Gravity.END else Gravity.START) + if (!inSplitSelection) { + if (splitConfig.initiatedFromSeascape) { + if (oneIconHiddenDueToSmallWidth) { + // Center both icons + val centerX = bottomToMidpointOffset - (taskIconHeight / 2f) + primaryIconView.translationX = centerX + secondaryIconView.translationX = centerX + } else { + // the task on the right (secondary) is slightly larger + primaryIconView.translationX = + (bottomToMidpointOffset - taskIconHeight).toFloat() + secondaryIconView.translationX = bottomToMidpointOffset.toFloat() + } + } else { + if (oneIconHiddenDueToSmallWidth) { + // Center both icons + val centerX = + bottomToMidpointOffset + insetOffset - (taskIconHeight / 2f) + primaryIconView.translationX = centerX + secondaryIconView.translationX = centerX + } else { + // the task on the left (primary) is slightly larger + primaryIconView.translationX = + (bottomToMidpointOffset + insetOffset - taskIconHeight).toFloat() + secondaryIconView.translationX = + (bottomToMidpointOffset + insetOffset).toFloat() + } + } + } + } else { + primaryIconParams.gravity = + Gravity.TOP or (if (isRtl) Gravity.START else Gravity.END) + secondaryIconParams.gravity = + Gravity.TOP or (if (isRtl) Gravity.START else Gravity.END) + if (!inSplitSelection) { + if (!splitConfig.initiatedFromSeascape) { + if (oneIconHiddenDueToSmallWidth) { + // Center both icons + val centerX = -bottomToMidpointOffset + (taskIconHeight / 2f) + primaryIconView.translationX = centerX + secondaryIconView.translationX = centerX + } else { + // the task on the left (primary) is slightly larger + primaryIconView.translationX = -bottomToMidpointOffset.toFloat() + secondaryIconView.translationX = + (-bottomToMidpointOffset + taskIconHeight).toFloat() + } + } else { + if (oneIconHiddenDueToSmallWidth) { + // Center both icons + val centerX = + -bottomToMidpointOffset - insetOffset + (taskIconHeight / 2f) + primaryIconView.translationX = centerX + secondaryIconView.translationX = centerX + } else { + // the task on the right (secondary) is slightly larger + primaryIconView.translationX = + (-bottomToMidpointOffset - insetOffset).toFloat() + secondaryIconView.translationX = + (-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat() + } + } + } + } + } else { + primaryIconParams.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL + secondaryIconParams.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL + if (!inSplitSelection) { + if (oneIconHiddenDueToSmallWidth) { + // Center both icons + primaryIconView.translationX = 0f + secondaryIconView.translationX = 0f + } else { + // shifts icon half a width left (height is used here since icons are square) + primaryIconView.translationX = -(taskIconHeight / 2f) + secondaryIconView.translationX = taskIconHeight / 2f + } + } + } + if (!enableOverviewIconMenu() && !inSplitSelection) { + primaryIconView.translationY = 0f + secondaryIconView.translationY = 0f + } + + primaryIconView.layoutParams = primaryIconParams + secondaryIconView.layoutParams = secondaryIconParams + } + + override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int { + check(deviceProfile.deviceProperties.isTablet) { + "Default position available only for large screens" + } + return if (deviceProfile.isLeftRightSplit) { + SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT + } else { + SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT + } + } + + override fun getSplitSelectTaskOffset( + primary: FloatProperty, + secondary: FloatProperty, + deviceProfile: DeviceProfile, + ): Pair, FloatProperty> = + if (deviceProfile.isLeftRightSplit) { // or seascape + Pair(primary, secondary) + } else { + Pair(secondary, primary) + } + + override fun getFloatingTaskOffscreenTranslationTarget( + floatingTask: View, + onScreenRect: RectF, + @StagePosition stagePosition: Int, + dp: DeviceProfile, + ): Float { + if (dp.isLeftRightSplit) { + val currentTranslationX = floatingTask.translationX + return if (stagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) + currentTranslationX - onScreenRect.width() + else currentTranslationX + onScreenRect.width() + } else { + val currentTranslationY = floatingTask.translationY + return currentTranslationY - onScreenRect.height() + } + } + + override fun setFloatingTaskPrimaryTranslation( + floatingTask: View, + translation: Float, + dp: DeviceProfile, + ) { + if (dp.isLeftRightSplit) { + floatingTask.translationX = translation + } else { + floatingTask.translationY = translation + } + } + + override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float = + if (dp.isLeftRightSplit) floatingTask.translationX else floatingTask.translationY + + override fun getHandlerTypeForLogging(): LauncherAtom.TaskSwitcherContainer.OrientationHandler = + LauncherAtom.TaskSwitcherContainer.OrientationHandler.PORTRAIT +} diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt index df4b03038f..7313eebe8f 100644 --- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt @@ -31,10 +31,10 @@ import com.android.launcher3.touch.PagedOrientationHandler import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction import com.android.launcher3.touch.SingleAxisSwipeDetector -import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption import com.android.launcher3.util.SplitConfigurationOptions.StagePosition import com.android.quickstep.views.IconAppChipView +import com.android.wm.shell.shared.split.SplitBounds /** * Abstraction layer to separate horizontal and vertical specific implementations for @@ -50,6 +50,8 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { fun getPrimarySize(rect: RectF): Float + fun getSecondarySize(rect: RectF): Float + val secondaryTranslationDirectionFactor: Int val degreesRotated: Float @@ -82,13 +84,13 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { fun getSplitTranslationDirectionFactor( @StagePosition stagePosition: Int, - deviceProfile: DeviceProfile + deviceProfile: DeviceProfile, ): Int fun getSplitSelectTaskOffset( primary: FloatProperty, secondary: FloatProperty, - deviceProfile: DeviceProfile + deviceProfile: DeviceProfile, ): Pair, FloatProperty> fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int @@ -101,7 +103,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { placeholderInset: Int, dp: DeviceProfile, @StagePosition stagePosition: Int, - out: Rect + out: Rect, ) /** @@ -128,7 +130,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { drawableWidth: Int, drawableHeight: Int, dp: DeviceProfile, - @StagePosition stagePosition: Int + @StagePosition stagePosition: Int, ) /** @@ -143,7 +145,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { out: View, dp: DeviceProfile, splitInstructionsHeight: Int, - splitInstructionsWidth: Int + splitInstructionsWidth: Int, ) /** @@ -159,7 +161,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { dp: DeviceProfile, @StagePosition stagePosition: Int, out1: Rect, - out2: Rect + out2: Rect, ) fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int @@ -173,8 +175,8 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { fun setSplitTaskSwipeRect( dp: DeviceProfile, outRect: Rect, - splitInfo: SplitConfigurationOptions.SplitBounds, - @StagePosition desiredStagePosition: Int + splitInfo: SplitBounds, + @StagePosition desiredStagePosition: Int, ) fun measureGroupedTaskViewThumbnailBounds( @@ -182,9 +184,10 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { secondarySnapshot: View, parentWidth: Int, parentHeight: Int, - splitBoundsConfig: SplitConfigurationOptions.SplitBounds, + splitBoundsConfig: SplitBounds, dp: DeviceProfile, - isRtl: Boolean + isRtl: Boolean, + inSplitSelection: Boolean, ) /** @@ -195,10 +198,11 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { */ fun getGroupedTaskViewSizes( dp: DeviceProfile, - splitBoundsConfig: SplitConfigurationOptions.SplitBounds, + splitBoundsConfig: SplitBounds, parentWidth: Int, - parentHeight: Int + parentHeight: Int, ): Pair + // Overview TaskMenuView methods /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */ fun setTaskIconParams( @@ -206,7 +210,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { taskIconMargin: Int, taskIconHeight: Int, thumbnailTopMargin: Int, - isRtl: Boolean + isRtl: Boolean, ) /** @@ -214,14 +218,14 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { */ fun setIconAppChipChildrenParams( iconParams: FrameLayout.LayoutParams, - chipChildMarginStart: Int + chipChildMarginStart: Int, ) fun setIconAppChipMenuParams( iconAppChipView: IconAppChipView, iconMenuParams: FrameLayout.LayoutParams, iconMenuMargin: Int, - thumbnailTopMargin: Int + thumbnailTopMargin: Int, ) fun setSplitIconParams( @@ -234,7 +238,9 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { groupedTaskViewWidth: Int, isRtl: Boolean, deviceProfile: DeviceProfile, - splitConfig: SplitConfigurationOptions.SplitBounds + splitConfig: SplitBounds, + inSplitSelection: Boolean, + oneIconHiddenDueToSmallWidth: Boolean, ) /* @@ -248,7 +254,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { thumbnailView: View, deviceProfile: DeviceProfile, taskInsetMargin: Float, - taskViewIcon: View + taskViewIcon: View, ): Float fun getTaskMenuY( @@ -257,20 +263,24 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { stagePosition: Int, taskMenuView: View, taskInsetMargin: Float, - taskViewIcon: View + taskViewIcon: View, ): Float + fun getAppChipMenuMarginX(appChipView: IconAppChipView, isRtl: Boolean): Int + + fun getAppChipMenuMarginY(appChipView: IconAppChipView, isRtl: Boolean): Int + fun getTaskMenuWidth( thumbnailView: View, deviceProfile: DeviceProfile, - @StagePosition stagePosition: Int + @StagePosition stagePosition: Int, ): Int fun getTaskMenuHeight( taskInsetMargin: Float, deviceProfile: DeviceProfile, taskMenuX: Float, - taskMenuY: Float + taskMenuY: Float, ): Int /** @@ -281,7 +291,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { deviceProfile: DeviceProfile, taskMenuLayout: LinearLayout, dividerSpacing: Int, - dividerDrawable: ShapeDrawable + dividerDrawable: ShapeDrawable, ) /** @@ -291,24 +301,36 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { fun setLayoutParamsForTaskMenuOptionItem( lp: LinearLayout.LayoutParams, viewGroup: LinearLayout, - deviceProfile: DeviceProfile + deviceProfile: DeviceProfile, + ) + + /** Layout a Digital Wellbeing Banner on its parent. TaskView. */ + fun updateDwbBannerLayout( + taskViewWidth: Int, + taskViewHeight: Int, + isGroupedTaskView: Boolean, + deviceProfile: DeviceProfile, + snapshotViewWidth: Int, + snapshotViewHeight: Int, + banner: View, ) /** - * Calculates the position where a Digital Wellbeing Banner should be placed on its parent + * Calculates the translations where a Digital Wellbeing Banner should be apply on its parent * TaskView. * * @return A Pair of Floats representing the proper x and y translations. */ - fun getDwbLayoutTranslations( + fun getDwbBannerTranslations( taskViewWidth: Int, taskViewHeight: Int, - splitBounds: SplitConfigurationOptions.SplitBounds?, + splitBounds: SplitBounds?, deviceProfile: DeviceProfile, thumbnailViews: Array, desiredTaskId: Int, - banner: View + banner: View, ): Pair + // The following are only used by TaskViewTouchHandler. /** @return Either VERTICAL or HORIZONTAL. */ @@ -317,12 +339,30 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { /** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is up. */ fun getUpDirection(isRtl: Boolean): Int + /** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is down. */ + fun getDownDirection(isRtl: Boolean): Int + /** @return Whether the displacement is going towards the top of the screen. */ fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */ fun getTaskDragDisplacementFactor(isRtl: Boolean): Int + /** @return Either 1 or -1, the direction sign towards task dismiss. */ + fun getTaskDismissVerticalDirection(): Int + + /** @return the length to drag a task off screen for dismiss. */ + fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int + + /** @return the length to drag a task to full screen for launch. */ + fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int + + /** Extends the provided rect to account for task dismiss primary translation. */ + fun extendRectForPrimaryTranslation(rect: Rect, translation: Int) + + /** Extends the provided rect to account for task dismiss secondary translation. */ + fun extendRectForSecondaryTranslation(rect: Rect, translation: Int) + /** * Maps the velocity from the coordinate plane of the foreground app to that of Launcher's * (which now will always be portrait) @@ -352,7 +392,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { floatingTask: View, onScreenRect: RectF, @StagePosition stagePosition: Int, - dp: DeviceProfile + dp: DeviceProfile, ): Float /** diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt index 333359fdd8..386ea5c22c 100644 --- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt @@ -25,23 +25,26 @@ import android.view.Gravity import android.view.Surface import android.view.View import android.view.View.MeasureSpec +import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.util.component1 import androidx.core.util.component2 +import androidx.core.view.marginStart +import androidx.core.view.updateLayoutParams import com.android.launcher3.DeviceProfile -import com.android.launcher3.Flags import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.logger.LauncherAtom import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption import com.android.launcher3.views.BaseDragLayer import com.android.quickstep.views.IconAppChipView +import com.android.wm.shell.shared.split.SplitBounds class SeascapePagedViewHandler : LandscapePagedViewHandler() { override fun rotateInsets(insets: Rect, outInsets: Rect) { @@ -52,7 +55,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { override fun getSplitTranslationDirectionFactor( stagePosition: Int, - deviceProfile: DeviceProfile + deviceProfile: DeviceProfile, ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1 override fun getRecentsRtlSetting(resources: Resources): Boolean = Utilities.isRtl(resources) @@ -69,8 +72,14 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { thumbnailView: View, deviceProfile: DeviceProfile, taskInsetMargin: Float, - taskViewIcon: View - ): Float = x + taskInsetMargin + taskViewIcon: View, + ): Float = + if (enableOverviewIconMenu()) { + taskViewIcon as IconAppChipView + x - taskViewIcon.menuToCollapsedChipGap + } else { + x + taskInsetMargin + } override fun getTaskMenuY( y: Float, @@ -78,10 +87,11 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { stagePosition: Int, taskMenuView: View, taskInsetMargin: Float, - taskViewIcon: View + taskViewIcon: View, ): Float { - if (Flags.enableOverviewIconMenu()) { - return y + if (enableOverviewIconMenu()) { + val marginStart = (taskViewIcon as IconAppChipView).backgroundMarginTopStart + return if (taskMenuView.isLayoutRtl) y + marginStart else y - marginStart } val lp = taskMenuView.layoutParams as BaseDragLayer.LayoutParams val taskMenuWidth = lp.width @@ -92,28 +102,27 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { } } + override fun getAppChipMenuMarginX(appChipView: IconAppChipView, isRtl: Boolean): Int = + -appChipView.menuToCollapsedChipGap + + override fun getAppChipMenuMarginY(appChipView: IconAppChipView, isRtl: Boolean): Int = + if (isRtl) -appChipView.backgroundMarginTopStart else appChipView.backgroundMarginTopStart + override fun getTaskMenuHeight( taskInsetMargin: Float, deviceProfile: DeviceProfile, taskMenuX: Float, - taskMenuY: Float - ): Int = (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX).toInt() + taskMenuY: Float, + ): Int = (deviceProfile.deviceProperties.availableWidthPx - taskInsetMargin - taskMenuX).toInt() override fun setSplitTaskSwipeRect( dp: DeviceProfile, outRect: Rect, splitInfo: SplitBounds, - desiredStagePosition: Int + desiredStagePosition: Int, ) { - val topLeftTaskPercent: Float - val dividerBarPercent: Float - if (splitInfo.appsStackedVertically) { - topLeftTaskPercent = splitInfo.topTaskPercent - dividerBarPercent = splitInfo.dividerHeightPercent - } else { - topLeftTaskPercent = splitInfo.leftTaskPercent - dividerBarPercent = splitInfo.dividerWidthPercent - } + val topLeftTaskPercent = splitInfo.leftTopTaskPercent + val dividerBarPercent = splitInfo.dividerPercent // In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of // the screen. This is to preserve consistency when the user rotates: From the user's POV, @@ -125,54 +134,60 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { } } - override fun getDwbLayoutTranslations( + override fun updateDwbBannerLayout( + taskViewWidth: Int, + taskViewHeight: Int, + isGroupedTaskView: Boolean, + deviceProfile: DeviceProfile, + snapshotViewWidth: Int, + snapshotViewHeight: Int, + banner: View, + ) { + banner.pivotX = 0f + banner.pivotY = 0f + banner.rotation = degreesRotated + banner.updateLayoutParams { + gravity = Gravity.BOTTOM or if (banner.isLayoutRtl) Gravity.END else Gravity.START + width = + if (isGroupedTaskView) { + snapshotViewHeight + } else { + taskViewHeight - deviceProfile.overviewProfile.taskThumbnailTopMarginPx + } + } + } + + override fun getDwbBannerTranslations( taskViewWidth: Int, taskViewHeight: Int, splitBounds: SplitBounds?, deviceProfile: DeviceProfile, thumbnailViews: Array, desiredTaskId: Int, - banner: View + banner: View, ): Pair { - val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams - val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL - - val bannerParams = banner.layoutParams as FrameLayout.LayoutParams - bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START - banner.pivotX = 0f - banner.pivotY = 0f - banner.rotation = degreesRotated - + val snapshotParams = thumbnailViews[0].layoutParams as ViewGroup.MarginLayoutParams val translationX: Float = (taskViewWidth - banner.height).toFloat() - if (splitBounds == null) { - // Single, fullscreen case - bannerParams.width = taskViewHeight - snapshotParams.topMargin - return Pair(translationX, banner.height.toFloat()) - } - - // Set correct width and translations val translationY: Float - if (desiredTaskId == splitBounds.leftTopTaskId) { - bannerParams.width = thumbnailViews[0].measuredHeight - val bottomRightTaskPlusDividerPercent = - if (splitBounds.appsStackedVertically) { - 1f - splitBounds.topTaskPercent - } else { - 1f - splitBounds.leftTaskPercent - } - translationY = - banner.height - - (taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent - } else { - bannerParams.width = thumbnailViews[1].measuredHeight + if (splitBounds == null) { translationY = banner.height.toFloat() + } else { + if (desiredTaskId == splitBounds.leftTopTaskId) { + val bottomRightTaskPlusDividerPercent = + splitBounds.rightBottomTaskPercent + splitBounds.dividerPercent + translationY = + banner.height - + (taskViewHeight - snapshotParams.topMargin) * + bottomRightTaskPlusDividerPercent + } else { + translationY = banner.height.toFloat() + } } - return Pair(translationX, translationY) } override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int = - dp.widthPx - rect.right + dp.deviceProperties.widthPx - rect.right override fun getSplitPositionOptions(dp: DeviceProfile): List = // Add "right" option which is actually the top @@ -181,7 +196,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, STAGE_POSITION_BOTTOM_OR_RIGHT, - STAGE_TYPE_MAIN + STAGE_TYPE_MAIN, ) ) @@ -189,7 +204,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { out: View, dp: DeviceProfile, splitInstructionsHeight: Int, - splitInstructionsWidth: Int + splitInstructionsWidth: Int, ) { out.pivotX = 0f out.pivotY = splitInstructionsHeight.toFloat() @@ -217,7 +232,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { taskIconMargin: Int, taskIconHeight: Int, thumbnailTopMargin: Int, - isRtl: Boolean + isRtl: Boolean, ) { iconParams.gravity = if (isRtl) { @@ -230,7 +245,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { override fun setIconAppChipChildrenParams( iconParams: FrameLayout.LayoutParams, - chipChildMarginStart: Int + chipChildMarginStart: Int, ) { iconParams.setMargins(0, 0, 0, 0) iconParams.marginStart = chipChildMarginStart @@ -241,20 +256,20 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { iconAppChipView: IconAppChipView, iconMenuParams: FrameLayout.LayoutParams, iconMenuMargin: Int, - thumbnailTopMargin: Int + thumbnailTopMargin: Int, ) { val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL val iconCenter = iconAppChipView.getHeight() / 2f if (isRtl) { - iconMenuParams.gravity = Gravity.TOP or Gravity.END + iconMenuParams.gravity = Gravity.TOP or Gravity.START iconMenuParams.topMargin = iconMenuMargin iconMenuParams.marginEnd = thumbnailTopMargin // Use half menu height to place the pivot within the X/Y center of icon in the menu. iconAppChipView.pivotX = iconMenuParams.width / 2f iconAppChipView.pivotY = iconMenuParams.width / 2f } else { - iconMenuParams.gravity = Gravity.BOTTOM or Gravity.START + iconMenuParams.gravity = Gravity.BOTTOM or Gravity.END iconMenuParams.topMargin = 0 iconMenuParams.marginEnd = 0 iconAppChipView.pivotX = iconCenter @@ -266,6 +281,10 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { iconAppChipView.setRotation(degreesRotated) } + /** + * @param inSplitSelection Whether user currently has a task from this task group staged for + * split screen. Currently this state is not reachable in fake seascape. + */ override fun measureGroupedTaskViewThumbnailBounds( primarySnapshot: View, secondarySnapshot: View, @@ -273,18 +292,19 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { parentHeight: Int, splitBoundsConfig: SplitBounds, dp: DeviceProfile, - isRtl: Boolean + isRtl: Boolean, + inSplitSelection: Boolean, ) { val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams // Swap the margins that are set in TaskView#setRecentsOrientedState() - secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx + secondaryParams.topMargin = dp.overviewProfile.taskThumbnailTopMarginPx primaryParams.topMargin = 0 // Measure and layout the thumbnails bottom up, since the primary is on the visual left // (portrait bottom) and secondary is on the right (portrait top) - val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx + val spaceAboveSnapshot = dp.overviewProfile.taskThumbnailTopMarginPx val totalThumbnailHeight = parentHeight - spaceAboveSnapshot val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) @@ -295,11 +315,11 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { (taskViewSecond.y + spaceAboveSnapshot + dividerBar).toFloat() primarySnapshot.measure( MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY) + MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY), ) secondarySnapshot.measure( MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY) + MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY), ) } @@ -307,20 +327,15 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { dp: DeviceProfile, splitBoundsConfig: SplitBounds, parentWidth: Int, - parentHeight: Int + parentHeight: Int, ): Pair { // Measure and layout the thumbnails bottom up, since the primary is on the visual left // (portrait bottom) and secondary is on the right (portrait top) - val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx + val spaceAboveSnapshot = dp.overviewProfile.taskThumbnailTopMarginPx val totalThumbnailHeight = parentHeight - spaceAboveSnapshot val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) - val taskPercent = - if (splitBoundsConfig.appsStackedVertically) { - splitBoundsConfig.topTaskPercent - } else { - splitBoundsConfig.leftTaskPercent - } + val taskPercent = splitBoundsConfig.leftTopTaskPercent val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt()) val secondTaskViewSize = Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar) @@ -335,10 +350,23 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE else SingleAxisSwipeDetector.DIRECTION_NEGATIVE + override fun getDownDirection(isRtl: Boolean): Int = + if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE + else SingleAxisSwipeDetector.DIRECTION_POSITIVE + override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean = if (isRtl) displacement > 0 else displacement < 0 override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1 + + override fun getTaskDismissVerticalDirection(): Int = -1 + + override fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int = + taskThumbnailBounds.right + + override fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int = + secondaryDimension - taskThumbnailBounds.right + /* -------------------- */ override fun getSplitIconsPosition( @@ -348,17 +376,18 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { isRtl: Boolean, overviewTaskMarginPx: Int, dividerSize: Int, + oneIconHiddenDueToSmallWidth: Boolean, ): SplitIconPositions { - return if (Flags.enableOverviewIconMenu()) { + return if (enableOverviewIconMenu()) { if (isRtl) { SplitIconPositions( topLeftY = totalThumbnailHeight - primarySnapshotHeight, - bottomRightY = 0 + bottomRightY = 0, ) } else { SplitIconPositions( topLeftY = 0, - bottomRightY = -(primarySnapshotHeight + dividerSize) + bottomRightY = -(primarySnapshotHeight + dividerSize), ) } } else { @@ -367,10 +396,16 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { // from the bottom to the almost-center of the screen using the bottom margin. // The primary snapshot is placed at the bottom, thus we translate the icons using // the size of the primary snapshot minus the icon size for the top-left icon. - SplitIconPositions( - topLeftY = primarySnapshotHeight - taskIconHeight, - bottomRightY = primarySnapshotHeight + dividerSize - ) + if (oneIconHiddenDueToSmallWidth) { + // Center both icons + val centerY = primarySnapshotHeight + ((dividerSize - taskIconHeight) / 2) + SplitIconPositions(topLeftY = centerY, bottomRightY = centerY) + } else { + SplitIconPositions( + topLeftY = primarySnapshotHeight - taskIconHeight, + bottomRightY = primarySnapshotHeight + dividerSize, + ) + } } } @@ -385,10 +420,10 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { override fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) { val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams - if (Flags.enableOverviewIconMenu()) { + if (enableOverviewIconMenu()) { val appChipView = iconView as IconAppChipView layoutParams.gravity = - if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START + if (isRtl) Gravity.TOP or Gravity.START else Gravity.BOTTOM or Gravity.END appChipView.layoutParams = layoutParams appChipView.setSplitTranslationX(0f) appChipView.setSplitTranslationY(translationY.toFloat()) diff --git a/quickstep/src/com/android/quickstep/recents/data/AppTimersRepository.kt b/quickstep/src/com/android/quickstep/recents/data/AppTimersRepository.kt new file mode 100644 index 0000000000..6932e763fe --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/AppTimersRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import android.os.UserHandle +import java.time.Duration + +/** Repository of information about the app usage timers. */ +interface AppTimersRepository { + /** Returns the remaining time on the app usage limit timer for a given user. */ + suspend fun getRemainingDuration(packageName: String, userHandle: UserHandle): Duration? +} diff --git a/quickstep/src/com/android/quickstep/recents/data/AppTimersRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/AppTimersRepositoryImpl.kt new file mode 100644 index 0000000000..930bbe44c6 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/AppTimersRepositoryImpl.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import android.content.pm.LauncherApps +import android.os.UserHandle +import com.android.launcher3.util.coroutines.DispatcherProvider +import java.time.Duration +import kotlinx.coroutines.withContext + +/** + * An [AppTimersRepository] that uses [LauncherApps] service to get information about app timers. + */ +class AppTimersRepositoryImpl( + private val dataSource: LauncherApps, + private val dispatcherProvider: DispatcherProvider, +) : AppTimersRepository { + + /** Returns the remaining time on the app usage timer set by the user. */ + override suspend fun getRemainingDuration( + packageName: String, + userHandle: UserHandle, + ): Duration? = + withContext(dispatcherProvider.ioBackground) { + val appUsageLimit = + dataSource.getAppUsageLimit(packageName, userHandle) ?: return@withContext null + + Duration.ofMillis(appUsageLimit.usageRemaining) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt new file mode 100644 index 0000000000..ad2bd253de --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt @@ -0,0 +1,28 @@ +/* + * 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.quickstep.recents.data + +import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback + +/** Notifies added callbacks that high res state has changed */ +interface HighResLoadingStateNotifier { + /** Adds a callback for high res loading state */ + fun addCallback(callback: HighResLoadingStateChangedCallback) + + /** Removes a callback for high res loading state */ + fun removeCallback(callback: HighResLoadingStateChangedCallback) +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt index c1eef0b393..f7b1674443 100644 --- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt +++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt @@ -17,21 +17,34 @@ package com.android.quickstep.recents.data import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData import kotlinx.coroutines.flow.Flow interface RecentTasksRepository { /** Gets all the recent tasks, refreshing from data sources if [forceRefresh] is true. */ - fun getAllTaskData(forceRefresh: Boolean = false): Flow> + fun getAllTaskData(displayId: Int, forceRefresh: Boolean = false): Flow> /** * Gets the data associated with a task that has id [taskId]. Flow will settle on null if the - * task was not found. + * task was not found. [Task.thumbnail] will settle on null if task is invisible. */ fun getTaskDataById(taskId: Int): Flow + /** + * Gets the [ThumbnailData] associated with a task that has id [taskId]. Flow will settle on + * null if the task was not found or is invisible. + */ + fun getThumbnailById(taskId: Int): Flow + + /** + * Gets the [ThumbnailData] associated with a task that has id [taskId]. Flow will settle on + * null if the task was not found or is invisible. + */ + fun getCurrentThumbnailById(taskId: Int): ThumbnailData? + /** * Sets the tasks that are visible, indicating that properties relating to visuals need to be * populated e.g. icons/thumbnails etc. */ - fun setVisibleTasks(visibleTaskIdList: List) + fun setVisibleTasks(displayId: Int, visibleTaskIdList: Set) } diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt new file mode 100644 index 0000000000..0ee2bd224c --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfile.kt @@ -0,0 +1,25 @@ +/* + * 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.quickstep.recents.data + +/** + * Container to hold [com.android.launcher3.DeviceProfile] related to Recents. + * + * @property isLargeScreen whether the current device posture has a large screen + * @property canEnterDesktopMode whether the current device can enter Desktop UI mode + */ +data class RecentsDeviceProfile(val isLargeScreen: Boolean, val canEnterDesktopMode: Boolean) diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt new file mode 100644 index 0000000000..13cf56d5b2 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepository.kt @@ -0,0 +1,21 @@ +/* + * 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.quickstep.recents.data + +interface RecentsDeviceProfileRepository { + fun getRecentsDeviceProfile(): RecentsDeviceProfile +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt new file mode 100644 index 0000000000..5397638ce1 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsDeviceProfileRepositoryImpl.kt @@ -0,0 +1,36 @@ +/* + * 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.quickstep.recents.data + +import com.android.quickstep.views.RecentsViewContainer +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus + +/** + * Repository for shrink down version of [com.android.launcher3.DeviceProfile] that only contains + * data related to Recents. + */ +class RecentsDeviceProfileRepositoryImpl(private val container: RecentsViewContainer) : + RecentsDeviceProfileRepository { + + override fun getRecentsDeviceProfile() = + with(container.deviceProfile) { + RecentsDeviceProfile( + isLargeScreen = deviceProperties.isTablet, + canEnterDesktopMode = DesktopModeStatus.canEnterDesktopMode(container.asContext()), + ) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt new file mode 100644 index 0000000000..2c2a744ed4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationState.kt @@ -0,0 +1,29 @@ +/* + * 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.quickstep.recents.data + +import android.view.Surface + +/** + * Container to hold orientation/rotation related information related to Recents. + * + * @property activityRotation rotation of the activity hosting RecentsView + */ +data class RecentsRotationState( + @Surface.Rotation val activityRotation: Int, + @Surface.Rotation val orientationHandlerRotation: Int, +) diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt new file mode 100644 index 0000000000..ed074d2e08 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepository.kt @@ -0,0 +1,21 @@ +/* + * 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.quickstep.recents.data + +interface RecentsRotationStateRepository { + fun getRecentsRotationState(): RecentsRotationState +} diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt new file mode 100644 index 0000000000..8417b061df --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImpl.kt @@ -0,0 +1,34 @@ +/* + * 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.quickstep.recents.data + +import com.android.quickstep.util.RecentsOrientedState + +/** + * Repository for [RecentsRotationState] which holds orientation/rotation related information + * related to Recents + */ +class RecentsRotationStateRepositoryImpl(private val state: RecentsOrientedState) : + RecentsRotationStateRepository { + override fun getRecentsRotationState() = + with(state) { + RecentsRotationState( + activityRotation = recentsActivityRotation, + orientationHandlerRotation = orientationHandler.rotation + ) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt new file mode 100644 index 0000000000..6e7789d7f8 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangeNotifier.kt @@ -0,0 +1,28 @@ +/* + * 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.quickstep.recents.data + +import com.android.quickstep.util.TaskVisualsChangeListener + +/** Notifies added listeners that task visuals have changed */ +interface TaskVisualsChangeNotifier { + /** Adds a listener for visuals changes */ + fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) + + /** Removes a listener for visuals changes */ + fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt new file mode 100644 index 0000000000..12616a8f84 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt @@ -0,0 +1,151 @@ +/* + * 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.quickstep.recents.data + +import android.os.UserHandle +import android.util.Log +import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.quickstep.util.TaskVisualsChangeListener +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import java.util.concurrent.ConcurrentHashMap + +/** Delegates the checking of task visuals (thumbnails, high res changes, icons) */ +interface TaskVisualsChangedDelegate : + TaskVisualsChangeListener, HighResLoadingStateChangedCallback { + /** Registers a callback for visuals relating to icons */ + fun registerTaskIconChangedCallback( + taskKey: Task.TaskKey, + taskIconChangedCallback: TaskIconChangedCallback, + ) + + /** Unregisters a callback for visuals relating to icons */ + fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) + + /** Registers a callback for visuals relating to thumbnails */ + fun registerTaskThumbnailChangedCallback( + taskKey: Task.TaskKey, + taskThumbnailChangedCallback: TaskThumbnailChangedCallback, + ) + + /** Unregisters a callback for visuals relating to thumbnails */ + fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) + + /** A callback for task icon changes */ + interface TaskIconChangedCallback { + /** Informs the listener that the task icon has changed */ + fun onTaskIconChanged() + } + + /** A callback for task thumbnail changes */ + interface TaskThumbnailChangedCallback { + /** Informs the listener that the task thumbnail data has changed to [thumbnailData] */ + fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) + + /** Informs the listener that the default resolution for loading thumbnails has changed */ + fun onHighResLoadingStateChanged(highResEnabled: Boolean) + } +} + +class TaskVisualsChangedDelegateImpl( + private val taskVisualsChangeNotifier: TaskVisualsChangeNotifier, + private val highResLoadingStateNotifier: HighResLoadingStateNotifier, +) : TaskVisualsChangedDelegate { + private val taskIconChangedCallbacks = + ConcurrentHashMap>() + private val taskThumbnailChangedCallbacks = + ConcurrentHashMap>() + + override fun onTaskIconChanged(taskId: Int) { + taskIconChangedCallbacks[taskId]?.let { (_, callback) -> callback.onTaskIconChanged() } + } + + override fun onTaskIconChanged(pkg: String, user: UserHandle) { + taskIconChangedCallbacks.values + .filter { (taskKey, _) -> + pkg == taskKey.packageName && user.identifier == taskKey.userId + } + .forEach { (_, callback) -> callback.onTaskIconChanged() } + } + + override fun onTaskThumbnailChanged(taskId: Int, thumbnailData: ThumbnailData?): Task? { + taskThumbnailChangedCallbacks[taskId]?.let { (_, callback) -> + callback.onTaskThumbnailChanged(thumbnailData) + } + return null + } + + override fun onHighResLoadingStateChanged(enabled: Boolean) { + Log.d(TAG, "onHighResLoadingStateChanged(enabled = $enabled)") + taskThumbnailChangedCallbacks.values.forEach { (_, callback) -> + callback.onHighResLoadingStateChanged(enabled) + } + } + + override fun registerTaskIconChangedCallback( + taskKey: Task.TaskKey, + taskIconChangedCallback: TaskIconChangedCallback, + ) { + updateCallbacks { + taskIconChangedCallbacks[taskKey.id] = taskKey to taskIconChangedCallback + } + } + + override fun unregisterTaskIconChangedCallback(taskKey: Task.TaskKey) { + updateCallbacks { taskIconChangedCallbacks.remove(taskKey.id) } + } + + override fun registerTaskThumbnailChangedCallback( + taskKey: Task.TaskKey, + taskThumbnailChangedCallback: TaskThumbnailChangedCallback, + ) { + updateCallbacks { + taskThumbnailChangedCallbacks[taskKey.id] = taskKey to taskThumbnailChangedCallback + } + } + + override fun unregisterTaskThumbnailChangedCallback(taskKey: Task.TaskKey) { + updateCallbacks { taskThumbnailChangedCallbacks.remove(taskKey.id) } + } + + @Synchronized + private fun updateCallbacks(callbackModifier: () -> Unit) { + val prevHasCallbacks = + taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size > 0 + callbackModifier() + + val currHasCallbacks = + taskIconChangedCallbacks.size + taskThumbnailChangedCallbacks.size > 0 + + when { + prevHasCallbacks && !currHasCallbacks -> { + taskVisualsChangeNotifier.removeThumbnailChangeListener(this) + highResLoadingStateNotifier.removeCallback(this) + } + !prevHasCallbacks && currHasCallbacks -> { + taskVisualsChangeNotifier.addThumbnailChangeListener(this) + highResLoadingStateNotifier.addCallback(this) + } + } + } + + companion object { + const val TAG = "TaskVisualsChangedDelegateImpl" + } +} diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt index b21a1b4add..74aaeff09b 100644 --- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt +++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt @@ -16,99 +16,233 @@ package com.android.quickstep.recents.data -import com.android.quickstep.TaskIconCache +import android.graphics.drawable.Drawable +import android.util.Log +import android.util.SparseArray +import androidx.core.util.valueIterator +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.quickstep.task.thumbnail.data.TaskIconDataSource import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource -import com.android.quickstep.util.GroupTask import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData -import kotlin.coroutines.resume -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.map -import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -@OptIn(ExperimentalCoroutinesApi::class) class TasksRepository( private val recentsModel: RecentTasksDataSource, private val taskThumbnailDataSource: TaskThumbnailDataSource, - private val taskIconCache: TaskIconCache, + private val taskIconDataSource: TaskIconDataSource, + private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate, + private val recentsCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, ) : RecentTasksRepository { - private val groupedTaskData = MutableStateFlow(emptyList()) - private val _taskData = - groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } } - private val visibleTaskIds = MutableStateFlow(emptySet()) + private val tasks = MutableStateFlow(MapForStateFlow(emptyMap())) + private var visibleTaskIdsPerDisplay = SparseArray>() + private val taskRequests = HashMap>() - private val taskData: Flow> = - combine(_taskData, getThumbnailQueryResults()) { tasks, results -> - tasks.forEach { task -> - // Add retrieved thumbnails + remove unnecessary thumbnails - task.thumbnail = results[task.key.id] - } - tasks + override fun getAllTaskData(displayId: Int, forceRefresh: Boolean): Flow> { + if (!visibleTaskIdsPerDisplay.contains(displayId)) { + visibleTaskIdsPerDisplay.put(displayId, emptySet()) } - - override fun getAllTaskData(forceRefresh: Boolean): Flow> { if (forceRefresh) { - recentsModel.getTasks { groupedTaskData.value = it } - } - return taskData - } - - override fun getTaskDataById(taskId: Int): Flow = - taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } } - - override fun setVisibleTasks(visibleTaskIdList: List) { - this.visibleTaskIds.value = visibleTaskIdList.toSet() - } - - /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */ - private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = - flow { - emit(task.key.id to task.thumbnail) - val thumbnailDataResult: ThumbnailData? = - suspendCancellableCoroutine { continuation -> - val cancellableTask = - taskThumbnailDataSource.updateThumbnailInBackground(task) { - continuation.resume(it) - } - continuation.invokeOnCancellation { cancellableTask?.cancel() } + recentsModel.getTasks { newTaskList -> + val recentTasks = + newTaskList.flatMap { groupTask -> groupTask.tasks }.associateBy { it.key.id } + Log.d( + TAG, + "getAllTaskData: oldTasks ${tasks.value.keys}, newTasks: ${recentTasks.keys}", + ) + tasks.update { oldTaskList -> + // Copy retrieved visuals to new Task objects + recentTasks.forEach { (taskId, task) -> + task.thumbnail = oldTaskList[taskId]?.thumbnail + task.icon = oldTaskList[taskId]?.icon + task.title = oldTaskList[taskId]?.title + task.titleDescription = oldTaskList[taskId]?.titleDescription } - emit(task.key.id to thumbnailDataResult) - } - .distinctUntilChanged() + MapForStateFlow(recentTasks) + } - /** - * This is a Flow that makes a query for thumbnail data to the [taskThumbnailDataSource] for - * each visible task. It then collects the responses and returns them in a Map as soon as they - * are available. - */ - private fun getThumbnailQueryResults(): Flow> { - val visibleTasks = - combine(_taskData, visibleTaskIds) { tasks, visibleIds -> - tasks.filter { it.key.id in visibleIds } + updateTaskRequests() } - val visibleThumbnailDataRequests: Flow> = - visibleTasks.map { - it.map { visibleTask -> - val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail } - getThumbnailDataRequest(taskCopy) + } + return tasks.map { it.values.filter { it.key.displayId == displayId }.toList() } + } + + override fun getTaskDataById(taskId: Int) = tasks.map { it[taskId] } + + override fun getThumbnailById(taskId: Int) = + getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId } + + override fun getCurrentThumbnailById(taskId: Int) = tasks.value[taskId]?.thumbnail + + override fun setVisibleTasks(displayId: Int, visibleTaskIdList: Set) { + if (visibleTaskIdList.isEmpty()) { + visibleTaskIdsPerDisplay.remove(displayId) + } else { + visibleTaskIdsPerDisplay.put(displayId, visibleTaskIdList) + } + updateTaskRequests() + } + + @Synchronized + private fun updateTaskRequests() { + val allVisibleTaskIds = + visibleTaskIdsPerDisplay.valueIterator().asSequence().flatMap { it }.toSet() + val requestsNeeded = allVisibleTaskIds.intersect(tasks.value.keys) + + val taskRequestIds = taskRequests.keys + val requestsNoLongerNeeded = taskRequestIds.subtract(requestsNeeded) + val newlyRequestedTasks = requestsNeeded.subtract(taskRequestIds) + if (requestsNoLongerNeeded.isNotEmpty() || newlyRequestedTasks.isNotEmpty()) { + Log.d( + TAG, + "updateTaskRequests to: $requestsNeeded, " + + "removed: $requestsNoLongerNeeded, added: $newlyRequestedTasks", + ) + } + + // Remove tasks are no longer visible + removeTasks(requestsNoLongerNeeded) + // Add new tasks to be requested + newlyRequestedTasks.forEach { taskId -> requestTaskData(taskId) } + } + + private fun requestTaskData(taskId: Int) { + val task = tasks.value[taskId] ?: return + Log.i(TAG, "requestTaskData: $taskId") + taskRequests[taskId] = + Pair( + task.key, + recentsCoroutineScope.launch(dispatcherProvider.lightweightBackground) { + val thumbnailFetchDeferred = async { fetchThumbnail(task) } + val iconFetchDeferred = async { fetchIcon(task) } + awaitAll(thumbnailFetchDeferred, iconFetchDeferred) + }, + ) + } + + private fun removeTasks(tasksToRemove: Set) { + if (tasksToRemove.isEmpty()) return + + Log.i(TAG, "removeTasks: $tasksToRemove") + tasks.update { currentTasks -> + tasksToRemove.forEach { taskId -> + val request = taskRequests.remove(taskId) ?: return@forEach + val (taskKey, job) = request + job.cancel() + + // un-registering callbacks + taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(taskKey) + taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(taskKey) + + // Clearing Task to reduce memory footprint + currentTasks[taskId]?.apply { + thumbnail = null + icon = null + title = null + titleDescription = null } } - return visibleThumbnailDataRequests.flatMapLatest { - thumbnailRequestFlows: List -> - if (thumbnailRequestFlows.isEmpty()) { - flowOf(emptyMap()) - } else { - combine(thumbnailRequestFlows) { it.toMap() } - } + MapForStateFlow(currentTasks) } } -} -typealias ThumbnailDataRequest = Flow> + private suspend fun fetchIcon(task: Task) { + updateIcon(task.key.id, getIconFromDataSource(task)) + taskVisualsChangedDelegate.registerTaskIconChangedCallback( + task.key, + object : TaskIconChangedCallback { + override fun onTaskIconChanged() { + recentsCoroutineScope.launch(dispatcherProvider.lightweightBackground) { + updateIcon(task.key.id, getIconFromDataSource(task)) + } + } + }, + ) + } + + private suspend fun fetchThumbnail(task: Task) { + updateThumbnail(task.key.id, getThumbnailFromDataSource(task)) + taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback( + task.key, + object : TaskThumbnailChangedCallback { + override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) { + updateThumbnail(task.key.id, thumbnailData) + } + + override fun onHighResLoadingStateChanged(highResEnabled: Boolean) { + val isTaskVisible = taskRequests.containsKey(task.key.id) + if (!isTaskVisible) return + + val isCurrentThumbnailLowRes = + tasks.value[task.key.id]?.thumbnail?.reducedResolution + val isRequestedResHigherThanCurrent = + isCurrentThumbnailLowRes == null || + (isCurrentThumbnailLowRes && highResEnabled) + if (!isRequestedResHigherThanCurrent) return + + recentsCoroutineScope.launch(dispatcherProvider.lightweightBackground) { + updateThumbnail(task.key.id, getThumbnailFromDataSource(task)) + } + } + }, + ) + } + + private fun updateIcon(taskId: Int, iconData: IconData) { + tasks.update { currentTasks -> + currentTasks[taskId]?.apply { + icon = iconData.icon + titleDescription = iconData.contentDescription + title = iconData.title + } + MapForStateFlow(currentTasks) + } + } + + private fun updateThumbnail(taskId: Int, thumbnail: ThumbnailData?) { + tasks.update { currentTasks -> + currentTasks[taskId]?.thumbnail = thumbnail + MapForStateFlow(currentTasks) + } + } + + private suspend fun getThumbnailFromDataSource(task: Task) = + withContext(dispatcherProvider.lightweightBackground) { + taskThumbnailDataSource.getThumbnail(task) + } + + private suspend fun getIconFromDataSource(task: Task) = + withContext(dispatcherProvider.lightweightBackground) { + val iconCacheEntry = taskIconDataSource.getIcon(task) + IconData(iconCacheEntry.icon, iconCacheEntry.contentDescription, iconCacheEntry.title) + } + + companion object { + private const val TAG = "TasksRepository" + } + + /** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */ + private data class MapForStateFlow( + private val backingMap: Map, + private val updated: Long = System.nanoTime(), + ) : Map by backingMap + + private data class IconData( + val icon: Drawable, + val contentDescription: String, + val title: String, + ) +} diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt new file mode 100644 index 0000000000..da235eef51 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt @@ -0,0 +1,335 @@ +/* + * 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.quickstep.recents.di + +import android.content.Context +import android.util.Log +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.RecentsModel +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate +import com.android.quickstep.recents.data.TaskVisualsChangedDelegateImpl +import com.android.quickstep.recents.data.TasksRepository +import com.android.quickstep.recents.domain.usecase.GetRemainingAppTimerDurationUseCase +import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase +import com.android.quickstep.recents.domain.usecase.GetTaskUseCase +import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase +import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper.PreviewPositionHelperFactory +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob + +internal typealias RecentsScopeId = String + +fun Any.toScopeId(): String = this as? RecentsScopeId ?: this.hashCode().toString() + +class RecentsDependencies +private constructor(appContext: Context, dispatcherProvider: DispatcherProvider) { + private val scopes = mutableMapOf() + + init { + startDefaultScope(appContext, dispatcherProvider) + } + + /** + * This function initialises the default scope with RecentsView dependencies. Some dependencies + * are global while others are per-RecentsView. The scope is used to differentiate between + * RecentsViews. + */ + private fun startDefaultScope(appContext: Context, dispatcherProvider: DispatcherProvider) { + Log.d(TAG, "startDefaultScope") + createScope(DEFAULT_SCOPE_ID).apply { + set(RecentsViewData::class.java.simpleName, RecentsViewData()) + val recentsCoroutineScope = + CoroutineScope( + SupervisorJob() + dispatcherProvider.unconfined + CoroutineName("RecentsView") + ) + set(CoroutineScope::class.java.simpleName, recentsCoroutineScope) + set(DispatcherProvider::class.java.simpleName, dispatcherProvider) + val recentsModel = RecentsModel.INSTANCE.get(appContext) + val taskVisualsChangedDelegate = + TaskVisualsChangedDelegateImpl( + recentsModel, + recentsModel.thumbnailCache.highResLoadingState, + ) + set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate) + + // Create RecentsTaskRepository singleton + val recentTasksRepository: RecentTasksRepository = + with(recentsModel) { + TasksRepository( + this, + thumbnailCache, + iconCache, + taskVisualsChangedDelegate, + recentsCoroutineScope, + dispatcherProvider, + ) + } + set(RecentTasksRepository::class.java.simpleName, recentTasksRepository) + } + } + + /** + * This function initialises a scope associated with the dependencies of a single RecentsView. + * + * @param viewContext the Context associated with a RecentsView. + * @return the scope id associated with the new RecentsDependenciesScope. + */ + fun createRecentsViewScope(viewContext: Context): String { + val scopeId = viewContext.toScopeId() + Log.d(TAG, "createRecentsViewScope $scopeId") + val scope = + createScope(scopeId).apply { + set(RecentsViewData::class.java.simpleName, RecentsViewData()) + val dispatcherProvider: DispatcherProvider = + get(DEFAULT_SCOPE_ID) + val recentsCoroutineScope = + CoroutineScope( + SupervisorJob() + + dispatcherProvider.unconfined + + CoroutineName("RecentsView$scopeId") + ) + set(CoroutineScope::class.java.simpleName, recentsCoroutineScope) + } + scope.linkTo(getScope(DEFAULT_SCOPE_ID)) + return scopeId + } + + inline fun inject( + scopeId: RecentsScopeId = "", + extras: RecentsDependenciesExtras = RecentsDependenciesExtras(), + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, + ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory) + + @Suppress("UNCHECKED_CAST") + @JvmOverloads + fun inject( + modelClass: Class, + scopeId: RecentsScopeId = DEFAULT_SCOPE_ID, + extras: RecentsDependenciesExtras = RecentsDependenciesExtras(), + factory: ((extras: RecentsDependenciesExtras) -> T)? = null, + ): T { + val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID } + val scope = scopes[currentScopeId] ?: createScope(currentScopeId) + + log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO) + var instance: T? + synchronized(this) { + instance = getDependency(scope, modelClass) + log("found instance? $instance", Log.INFO) + if (instance == null) { + instance = + factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras) + scope[modelClass.simpleName] = instance!! + log( + "instance of $modelClass" + + " (${instance.hashCode()}) added to scope ${scope.scopeId}" + ) + } + } + return instance!! + } + + inline fun provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T = + provide(T::class.java, scopeId = scopeId, factory = factory) + + @JvmOverloads + fun provide( + modelClass: Class, + scopeId: RecentsScopeId = DEFAULT_SCOPE_ID, + factory: () -> T, + ) = inject(modelClass, scopeId, factory = { factory.invoke() }) + + private fun getDependency(scope: RecentsDependenciesScope, modelClass: Class): T? { + var instance: T? = scope[modelClass.simpleName] as T? + if (instance == null) { + instance = + scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId -> + getScope(scopeId)[modelClass.simpleName] + } as T? + } + if (instance != null) log("Found dependency: $instance", Log.INFO) + return instance + } + + fun getScope(scope: Any) = getScope(scope.toScopeId()) + + fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope = + scopes[scopeId] ?: createScope(scopeId) + + fun removeScope(scope: Any) { + val scopeId = scope.toScopeId() + scopes[scopeId]?.close() + scopes.remove(scopeId) + log("Scope $scopeId removed") + } + + // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely. + // Each class should be responsible for providing a factory function to create a new instance. + @Suppress("UNCHECKED_CAST") + private fun createDependency( + modelClass: Class, + scopeId: RecentsScopeId, + extras: RecentsDependenciesExtras, + ): T { + log("createDependency ${modelClass.simpleName} with $scopeId and $extras started", Log.WARN) + log("linked scopes: ${getScope(scopeId).scopeIdsLinked}") + val instance: Any = + when (modelClass) { + IsThumbnailValidUseCase::class.java -> + IsThumbnailValidUseCase(rotationStateRepository = inject(scopeId)) + GetRemainingAppTimerDurationUseCase::class.java -> + GetRemainingAppTimerDurationUseCase(appTimersRepository = inject(scopeId)) + GetTaskUseCase::class.java -> + GetTaskUseCase( + tasksRepository = inject(scopeId), + getRemainingAppTimerDurationUseCase = inject(scopeId), + ) + GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase() + GetThumbnailPositionUseCase::class.java -> + GetThumbnailPositionUseCase( + deviceProfileRepository = inject(scopeId), + rotationStateRepository = inject(scopeId), + previewPositionHelperFactory = PreviewPositionHelperFactory(), + ) + OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase() + else -> { + log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR) + error("Factory for ${modelClass.simpleName} not defined!") + } + } + return (instance as T).also { + log( + "createDependency ${modelClass.simpleName} with $scopeId and $extras completed", + Log.WARN, + ) + } + } + + private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope { + return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it } + } + + private fun log(message: String, @Log.Level level: Int = Log.DEBUG) { + if (DEBUG) { + when (level) { + Log.WARN -> Log.w(TAG, message) + Log.VERBOSE -> Log.v(TAG, message) + Log.INFO -> Log.i(TAG, message) + Log.ERROR -> Log.e(TAG, message) + else -> Log.d(TAG, message) + } + } + } + + companion object { + const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope" + private const val TAG = "RecentsDependencies" + private const val DEBUG = false + + @Volatile private var instance: RecentsDependencies? = null + + private fun initialize( + context: Context, + dispatcherProvider: DispatcherProvider, + ): RecentsDependencies { + Log.d(TAG, "initializing") + synchronized(this) { + val newInstance = + RecentsDependencies(context.applicationContext, dispatcherProvider) + instance = newInstance + return newInstance + } + } + + @JvmStatic + fun maybeInitialize( + context: Context, + dispatcherProvider: DispatcherProvider, + ): RecentsDependencies { + return instance ?: initialize(context, dispatcherProvider) + } + + fun getInstance(): RecentsDependencies { + return instance + ?: throw UninitializedPropertyAccessException( + "Recents dependencies are not initialized. " + + "Call `RecentsDependencies.maybeInitialize` before using this container." + ) + } + + @JvmStatic + fun destroy(viewContext: Context) { + synchronized(this) { + val localInstance = instance ?: return + val scopeId = viewContext.toScopeId() + val scope = localInstance.scopes[scopeId] + if (scope == null) { + Log.e( + TAG, + "Trying to destroy an unknown scope. Scopes: ${localInstance.scopes.size}", + ) + return + } + scope.close() + localInstance.scopes.remove(scopeId) + if (DEBUG) { + Log.d(TAG, "destroyed $scopeId", Exception("Printing stack trace")) + } else { + Log.d(TAG, "destroyed $scopeId") + } + if (localInstance.scopes.size == 1) { + // Only the default scope left - destroy this too. + instance = null + Log.d(TAG, "also destroyed default scope") + } + } + } + + fun hasScope(scope: Any) = + synchronized(this) { + val localInstance = instance ?: return false + localInstance.scopes.containsKey(scope.toScopeId()) + } + } +} + +inline fun RecentsDependencies.Companion.inject( + scope: Any = "", + vararg extras: Pair, + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, +): Lazy = lazy { get(scope, RecentsDependenciesExtras(extras), factory) } + +inline fun RecentsDependencies.Companion.get( + scope: Any = "", + extras: RecentsDependenciesExtras = RecentsDependenciesExtras(), + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, +): T { + return getInstance().inject(scope.toScopeId(), extras, factory) +} + +inline fun RecentsDependencies.Companion.get( + scope: Any = "", + vararg extras: Pair, + noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null, +): T = get(scope, RecentsDependenciesExtras(extras), factory) + +fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId) diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt new file mode 100644 index 0000000000..753cb6e7c9 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.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.quickstep.recents.di + +data class RecentsDependenciesExtras(private val data: MutableMap = mutableMapOf()) { + constructor(value: Array>) : this(value.toMap().toMutableMap()) + + operator fun get(key: String) = data[key] + + operator fun set(key: String, value: Any) { + data[key] = value + } +} diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt new file mode 100644 index 0000000000..56bb1edfe9 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt @@ -0,0 +1,71 @@ +/* + * 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.quickstep.recents.di + +import android.util.Log + +class RecentsDependenciesScope( + val scopeId: RecentsScopeId, + private val dependencies: MutableMap = mutableMapOf(), + private val scopeIds: MutableList = mutableListOf() +) { + val scopeIdsLinked: List + get() = scopeIds.toList() + + operator fun get(identifier: String): Any? { + log("get $identifier") + return dependencies[identifier] + } + + operator fun set(key: String, value: Any) { + synchronized(this) { + log("set $key") + dependencies[key] = value + } + } + + fun remove(key: String): Any? { + synchronized(this) { + log("remove $key") + return dependencies.remove(key) + } + } + + fun linkTo(scope: RecentsDependenciesScope) { + log("linking to ${scope.scopeId}") + scopeIds += scope.scopeId + } + + fun close() { + log("reset") + synchronized(this) { dependencies.clear() } + } + + private fun log(message: String) { + if (DEBUG) Log.d(TAG, "[scopeId=$scopeId] $message") + } + + override fun toString(): String = + "scopeId: $scopeId" + + "\n dependencies: ${dependencies.map { "${it.key}=${it.value}" }.joinToString(", ")}" + + "\n linked to: ${scopeIds.joinToString(", ")}" + + private companion object { + private const val TAG = "RecentsDependenciesScope" + private const val DEBUG = false + } +} diff --git a/quickstep/src/com/android/quickstep/recents/domain/model/DesktopLayoutConfig.kt b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopLayoutConfig.kt new file mode 100644 index 0000000000..8c2d79519e --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopLayoutConfig.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.model + +/** + * Holds pre-scaled configuration values related to desktop task layout dimensions. These values are + * typically derived from resources and then scaled according to the current view and screen + * dimensions. + * + * @property topBottomMarginOneRow Scaled margin for top/bottom when one row is shown. + * @property topMarginMultiRows Scaled top margin when multiple rows are shown. + * @property bottomMarginMultiRows Scaled bottom margin when multiple rows are shown. + * @property leftRightMarginOneRow Scaled margin for left/right when one row is shown. + * @property leftRightMarginMultiRows Scaled margin for left/right when multiple rows are shown. + * @property horizontalPaddingBetweenTasks Scaled horizontal padding between tasks. + * @property verticalPaddingBetweenTasks Scaled vertical padding between tasks. + */ +data class DesktopLayoutConfig( + val topBottomMarginOneRow: Int, + val topMarginMultiRows: Int, + val bottomMarginMultiRows: Int, + val leftRightMarginOneRow: Int, + val leftRightMarginMultiRows: Int, + val horizontalPaddingBetweenTasks: Int, + val verticalPaddingBetweenTasks: Int, +) diff --git a/quickstep/src/com/android/quickstep/recents/domain/model/DesktopTaskBoundsData.kt b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopTaskBoundsData.kt new file mode 100644 index 0000000000..a7f102cf14 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopTaskBoundsData.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.model + +import android.graphics.Rect + +data class DesktopTaskBoundsData(val taskId: Int, val bounds: Rect) diff --git a/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt b/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt new file mode 100644 index 0000000000..05959e1138 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/model/TaskModel.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.model + +import android.graphics.drawable.Drawable +import com.android.systemui.shared.recents.model.ThumbnailData +import java.time.Duration + +/** + * Data class representing a task in the application. + * + * This class holds the essential information about a task, including its unique identifier, display + * title, associated icon, optional thumbnail data, and background color. + * + * @property id The unique identifier for this task. Must be an integer. + * @property packageName top activity package for the task's app + * @property title The display title of the task. + * @property titleDescription A content description of the task. + * @property icon An optional drawable resource representing an icon for the task. Can be null if no + * icon is required. + * @property thumbnail An optional [ThumbnailData] object containing thumbnail information. Can be + * null if no thumbnail is needed. + * @property backgroundColor The background color of the task, represented as an integer color + * value. + * @property isLocked Indicates whether the [Task] is locked. + * @property isMinimized Indicates whether the [Task] is minimized. + * @property remainingAppDuration time remaining on the app timer for the application. + */ +data class TaskModel( + val id: TaskId, + val packageName: String, + val title: String?, + val titleDescription: String?, + val icon: Drawable?, + val thumbnail: ThumbnailData?, + val backgroundColor: Int, + val isLocked: Boolean, + val isMinimized: Boolean, + val remainingAppDuration: Duration?, +) + +typealias TaskId = Int diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/GetRemainingAppTimerDurationUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetRemainingAppTimerDurationUseCase.kt new file mode 100644 index 0000000000..8128d1fe88 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetRemainingAppTimerDurationUseCase.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.os.UserHandle +import com.android.quickstep.recents.data.AppTimersRepository +import java.time.Duration + +/** + * Use case that provides remaining duration on the app usage limit timer set for the given user. + * + * Responsible for applying business rules around how the remaining time is treated within overview + * module e.g. rounds partial minutes to the next minute. + */ +class GetRemainingAppTimerDurationUseCase(private val appTimersRepository: AppTimersRepository) { + suspend operator fun invoke(packageName: String, userHandle: UserHandle): Duration? { + val totalRemainingDuration = + appTimersRepository.getRemainingDuration(packageName, userHandle) ?: return null + val totalRemainingMs = totalRemainingDuration.toMillis() + + val isLessThanAMinute = totalRemainingMs < MS_IN_A_MINUTE + val isInWholeMinutes = (totalRemainingMs % MS_IN_A_MINUTE) == 0L + return when { + isLessThanAMinute || isInWholeMinutes -> totalRemainingDuration + else -> Duration.ofMinutes(totalRemainingDuration.toMinutes() + 1) + } + } + + companion object { + private const val MS_IN_A_MINUTE: Int = 60000 + } +} diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCase.kt new file mode 100644 index 0000000000..da0e2d10fc --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCase.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV +import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS +import com.android.systemui.shared.recents.model.ThumbnailData + +/** UseCase to calculate flags for status bar and navigation bar */ +class GetSysUiStatusNavFlagsUseCase { + operator fun invoke(thumbnailData: ThumbnailData?): Int { + if (thumbnailData == null) return 0 + val thumbnailAppearance = thumbnailData.appearance + var flags = 0 + flags = + flags or + if (thumbnailAppearance and APPEARANCE_LIGHT_STATUS_BARS != 0) FLAG_LIGHT_STATUS + else FLAG_DARK_STATUS + flags = + flags or + if (thumbnailAppearance and APPEARANCE_LIGHT_NAVIGATION_BARS != 0) FLAG_LIGHT_NAV + else FLAG_DARK_NAV + return flags + } +} diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCase.kt new file mode 100644 index 0000000000..72d4ca65ab --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCase.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.os.UserHandle +import com.android.launcher3.Flags.enableRefactorDigitalWellbeingToast +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.quickstep.recents.domain.model.TaskModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetTaskUseCase( + private val tasksRepository: RecentTasksRepository, + private val getRemainingAppTimerDurationUseCase: GetRemainingAppTimerDurationUseCase, +) { + operator fun invoke(taskId: Int): Flow = + tasksRepository.getTaskDataById(taskId).map { task -> + if (task == null) return@map null + + val packageName = task.topComponent.packageName + + // TODO(b/405359794): If getTask for a single task ends up being called multiple + // times by the UI, explore alternatives of loading the timer info only once. + val remainingDuration = + if (enableRefactorDigitalWellbeingToast()) { + getRemainingAppTimerDurationUseCase( + packageName = packageName, + userHandle = UserHandle(task.key.userId), + ) + } else { + null + } + + TaskModel( + id = task.key.id, + packageName = packageName, + title = task.title, + titleDescription = task.titleDescription, + icon = task.icon, + thumbnail = task.thumbnail, + backgroundColor = task.colorBackground, + isLocked = task.isLocked, + isMinimized = task.isMinimized, + remainingAppDuration = remainingDuration, + ) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt new file mode 100644 index 0000000000..e83d9f0549 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Matrix +import android.graphics.Rect +import com.android.quickstep.recents.data.RecentsDeviceProfileRepository +import com.android.quickstep.recents.data.RecentsRotationStateRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper + +/** Use case for retrieving [Matrix] for positioning Thumbnail in a View */ +class GetThumbnailPositionUseCase( + private val deviceProfileRepository: RecentsDeviceProfileRepository, + private val rotationStateRepository: RecentsRotationStateRepository, + private val previewPositionHelperFactory: PreviewPositionHelper.PreviewPositionHelperFactory, +) { + operator fun invoke( + thumbnailData: ThumbnailData?, + width: Int, + height: Int, + isRtl: Boolean, + ): ThumbnailPosition { + val thumbnail = + thumbnailData?.thumbnail ?: return ThumbnailPosition(Matrix.IDENTITY_MATRIX, false) + + val previewPositionHelper = previewPositionHelperFactory.create() + previewPositionHelper.updateThumbnailMatrix( + Rect(0, 0, thumbnail.width, thumbnail.height), + thumbnailData, + width, + height, + deviceProfileRepository.getRecentsDeviceProfile().isLargeScreen, + rotationStateRepository.getRecentsRotationState().activityRotation, + isRtl, + ) + return ThumbnailPosition( + matrix = previewPositionHelper.matrix, + isRotated = previewPositionHelper.isOrientationChanged, + ) + } +} + +data class ThumbnailPosition(val matrix: Matrix, val isRotated: Boolean) diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt new file mode 100644 index 0000000000..02f8329c2e --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Bitmap +import android.view.Surface +import com.android.quickstep.recents.data.RecentsRotationStateRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.android.systemui.shared.recents.utilities.Utilities + +/** + * Use case responsible for validating the aspect ratio and rotation of a thumbnail against the + * expected values based on the view's dimensions and the current rotation state. + * + * This class checks if the thumbnail's aspect ratio significantly differs from the aspect ratio of + * the view it is intended to be displayed in, and if the thumbnail's rotation is consistent with + * the device's current rotation state. + * + * @property rotationStateRepository Repository providing the current rotation state of the device. + */ +class IsThumbnailValidUseCase(private val rotationStateRepository: RecentsRotationStateRepository) { + operator fun invoke(thumbnailData: ThumbnailData?, viewWidth: Int, viewHeight: Int): Boolean { + val thumbnail = thumbnailData?.thumbnail ?: return false + return !isInaccurateThumbnail(thumbnail, viewWidth, viewHeight, thumbnailData.rotation) + } + + private fun isInaccurateThumbnail( + thumbnail: Bitmap, + viewWidth: Int, + viewHeight: Int, + rotation: Int, + ): Boolean = + isAspectRatioDifferentFromViewAspectRatio( + thumbnail = thumbnail, + width = viewWidth.toFloat(), + height = viewHeight.toFloat(), + ) || isRotationDifferentFromTask(rotation) + + private fun isAspectRatioDifferentFromViewAspectRatio( + thumbnail: Bitmap, + width: Float, + height: Float, + ): Boolean { + return Utilities.isRelativePercentDifferenceGreaterThan( + /* first = */ width / height, + /* second = */ thumbnail.width / thumbnail.height.toFloat(), + /* bound = */ PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT, + ) + } + + private fun isRotationDifferentFromTask(thumbnailRotation: Int): Boolean { + val rotationState = rotationStateRepository.getRecentsRotationState() + return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) { + (rotationState.activityRotation - thumbnailRotation) % 2 != 0 + } else { + rotationState.orientationHandlerRotation != thumbnailRotation + } + } +} diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt new file mode 100644 index 0000000000..869677149f --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Rect +import android.graphics.RectF +import androidx.core.graphics.toRect +import com.android.quickstep.recents.domain.model.DesktopLayoutConfig +import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData + +/** This usecase is responsible for organizing desktop windows in a non-overlapping way. */ +class OrganizeDesktopTasksUseCase { + /** + * Run to layout [taskBounds] within the screen [desktopBounds]. Layout is done in 2 stages: + * 1. Optimal height is determined. In this stage height is bisected to find maximum height + * which still allows all the windows to fit. + * 2. Row widths are balanced. In this stage the available width is reduced until some windows + * are no longer fitting or until the difference between the narrowest and the widest rows + * starts growing. Overall this achieves the goals of maximum size for previews (or maximum + * row height which is equivalent assuming fixed height), balanced rows and minimal wasted + * space. + */ + operator fun invoke( + desktopBounds: Rect, + taskBounds: List, + layoutConfig: DesktopLayoutConfig, + ): List { + if (desktopBounds.isEmpty || taskBounds.isEmpty()) { + return emptyList() + } + + // Filter out [taskBounds] with empty rects before calculating layout. + val validTaskBounds = taskBounds.filterNot { it.bounds.isEmpty } + + if (validTaskBounds.isEmpty()) { + return emptyList() + } + + // Assuming we can place all windows in one row, do one pass first to check whether all + // windows can fit. + var availableLayoutBounds = + desktopBounds.getLayoutEffectiveBounds( + singleRow = true, + taskNumber = taskBounds.size, + layoutConfig, + ) + var resultRects = + findOptimalHeightAndBalancedWidth( + availableLayoutBounds, + validTaskBounds, + layoutConfig, + singleRow = true, + ) + + if (!canFitInOneRow(resultRects)) { + availableLayoutBounds = + desktopBounds.getLayoutEffectiveBounds( + singleRow = true, + taskNumber = taskBounds.size, + layoutConfig, + ) + resultRects = + findOptimalHeightAndBalancedWidth( + availableLayoutBounds, + validTaskBounds, + layoutConfig, + singleRow = true, + ) + } + + centerTaskWindows( + availableLayoutBounds, + resultRects.maxOf { it.bottom }.toInt(), + resultRects, + layoutConfig, + ) + + val result = mutableListOf() + for (i in validTaskBounds.indices) { + result.add(DesktopTaskBoundsData(validTaskBounds[i].taskId, resultRects[i].toRect())) + } + return result + } + + /** Calculates the effective bounds for layout by applying insets to the raw desktop bounds. */ + private fun Rect.getLayoutEffectiveBounds( + singleRow: Boolean, + taskNumber: Int, + layoutConfig: DesktopLayoutConfig, + ) = + Rect(this).apply { + val topInset = + if (singleRow) layoutConfig.topBottomMarginOneRow + else layoutConfig.topMarginMultiRows + val bottomInset = + if (singleRow) layoutConfig.topBottomMarginOneRow + else layoutConfig.bottomMarginMultiRows + val leftInset = + if (singleRow && taskNumber <= 1) layoutConfig.leftRightMarginOneRow + else layoutConfig.leftRightMarginMultiRows + val rightInset = + if (singleRow && taskNumber <= 1) leftInset + else (leftInset - layoutConfig.horizontalPaddingBetweenTasks) + + inset(leftInset, topInset, rightInset, bottomInset) + } + + /** Calculates the maximum height for a task window in the desktop tile in Overview. */ + private fun getMaxTaskHeight( + effectiveLayoutBounds: Rect, + layoutConfig: DesktopLayoutConfig, + singleRow: Boolean, + ) = + if (singleRow) { + effectiveLayoutBounds.height() + } else { + effectiveLayoutBounds.height() - 2 * layoutConfig.verticalPaddingBetweenTasks + } + + /** + * Determines the optimal height for task windows and balances the row widths to minimize wasted + * space. Returns the bounds for each task window after layout. + */ + private fun findOptimalHeightAndBalancedWidth( + availableLayoutBounds: Rect, + validTaskBounds: List, + layoutConfig: DesktopLayoutConfig, + singleRow: Boolean, + ): List { + // Right bound of the narrowest row. + var minRight: Int + // Right bound of the widest row. + var maxRight: Int + + // Keep track of the difference between the narrowest and the widest row. + // Initially this is set to the worst it can ever be assuming the windows fit. + var widthDiff = availableLayoutBounds.width() + + // Initially allow the windows to occupy all available width. Shrink this available space + // horizontally to find the breakdown into rows that achieves the minimal [widthDiff]. + var rightBound = availableLayoutBounds.right + + // Determine the optimal height bisecting between [lowHeight] and [highHeight]. Once this + // optimal height is known, [heightFixed] is set to `true` and the rows are balanced by + // repeatedly squeezing the widest row to cause windows to overflow to the subsequent rows. + var lowHeight = layoutConfig.verticalPaddingBetweenTasks + var highHeight = maxOf(lowHeight, availableLayoutBounds.height() + 1) + var optimalHeight = 0.5f * (lowHeight + highHeight) + var heightFixed = false + + // Repeatedly try to fit the windows [resultRects] within [rightBound]. If a maximum + // [optimalHeight] is found such that all window [resultRects] fit, this fitting continues + // while shrinking the [rightBound] in order to balance the rows. If the windows fit the + // [rightBound] would have been decremented at least once so it needs to be incremented once + // before getting out of this loop and one additional pass made to actually fit the + // [resultRects]. If the [resultRects] cannot fit (e.g. there are too many windows) the + // bisection will still finish and we might increment the [rightBound] one pixel extra + // which is acceptable since there is an unused margin on the right. + var makeLastAdjustment = false + var resultRects: List + + while (true) { + val fitWindowResult = + fitWindowRectsInBounds( + Rect(availableLayoutBounds).apply { right = rightBound }, + validTaskBounds, + minOf( + getMaxTaskHeight(availableLayoutBounds, layoutConfig, singleRow), + optimalHeight.toInt(), + ), + layoutConfig, + ) + val allWindowsFit = fitWindowResult.allWindowsFit + resultRects = fitWindowResult.calculatedBounds + minRight = fitWindowResult.minRight + maxRight = fitWindowResult.maxRight + + if (heightFixed) { + if (!allWindowsFit) { + // Revert the previous change to [rightBound] and do one last pass. + rightBound++ + makeLastAdjustment = true + break + } + // Break if all the windows are zero-width at the current scale. + if (maxRight <= availableLayoutBounds.left) { + break + } + } else { + // Find the optimal row height bisecting between [lowHeight] and [highHeight]. + if (allWindowsFit) { + lowHeight = optimalHeight.toInt() + } else { + highHeight = optimalHeight.toInt() + } + optimalHeight = 0.5f * (lowHeight + highHeight) + // When height can no longer be improved, start balancing the rows. + if (optimalHeight.toInt() == lowHeight) { + heightFixed = true + } + } + + if (allWindowsFit && heightFixed) { + if (maxRight - minRight <= widthDiff) { + // Row alignment is getting better. Try to shrink the [rightBound] in order to + // squeeze the widest row. + rightBound = maxRight - 1 + widthDiff = maxRight - minRight + } else { + // Row alignment is getting worse. + // Revert the previous change to [rightBound] and do one last pass. + rightBound++ + makeLastAdjustment = true + break + } + } + } + + // Once the windows no longer fit, the change to [rightBound] was reverted. Perform one last + // pass to position the [resultRects]. + if (makeLastAdjustment) { + val fitWindowResult = + fitWindowRectsInBounds( + Rect(availableLayoutBounds).apply { right = rightBound }, + validTaskBounds, + minOf( + getMaxTaskHeight(availableLayoutBounds, layoutConfig, singleRow), + optimalHeight.toInt(), + ), + layoutConfig, + ) + resultRects = fitWindowResult.calculatedBounds + } + + return resultRects + } + + /** + * Data structure to hold the returned result of [fitWindowRectsInBounds] function. + * [allWindowsFit] specifies whether all windows can be fit into the provided layout bounds. + * [calculatedBounds] specifies the output bounds for all provided task windows. [minRight] + * specifies the right bound of the narrowest row. [maxRight] specifies the right bound of the + * widest rows. + */ + data class FitWindowResult( + val allWindowsFit: Boolean, + val calculatedBounds: List, + val minRight: Int, + val maxRight: Int, + ) + + /** + * Attempts to fit all [taskBounds] inside [layoutBounds]. The method ensures that the returned + * output bounds list has appropriate size and populates it with the values placing task windows + * next to each other left-to-right in rows of equal [optimalWindowHeight]. + */ + private fun fitWindowRectsInBounds( + layoutBounds: Rect, + taskBounds: List, + optimalWindowHeight: Int, + layoutConfig: DesktopLayoutConfig, + ): FitWindowResult { + val numTasks = taskBounds.size + val outRects = mutableListOf() + + val verticalPadding = layoutConfig.verticalPaddingBetweenTasks + val horizontalPadding = layoutConfig.horizontalPaddingBetweenTasks + + // Start in the top-left corner of [layoutBounds]. + var left = layoutBounds.left + var top = layoutBounds.top + + // Right bound of the narrowest row. + var minRight = layoutBounds.right + // Right bound of the widest row. + var maxRight = layoutBounds.left + + var allWindowsFit = true + for (i in 0 until numTasks) { + val taskBounds = taskBounds[i].bounds + + // Use the height to calculate the width + val scale = optimalWindowHeight / taskBounds.height().toFloat() + val width = (taskBounds.width() * scale).toInt() + val optimalRowHeight = optimalWindowHeight + verticalPadding + + if (left + width + horizontalPadding > layoutBounds.right) { + // Move to the next row if possible. + minRight = minOf(minRight, left) + maxRight = maxOf(maxRight, left) + top += optimalRowHeight + + // Check if the new row reaches the bottom or if the first item in the new + // row does not fit within the available width. + if ( + (top + optimalRowHeight) > layoutBounds.bottom || + layoutBounds.left + width + horizontalPadding > layoutBounds.right + ) { + allWindowsFit = false + break + } + left = layoutBounds.left + } + + // Position the current rect. + outRects.add( + RectF( + left.toFloat(), + top.toFloat(), + (left + width).toFloat(), + (top + optimalWindowHeight).toFloat(), + ) + ) + + // Increment horizontal position. + left += (width + horizontalPadding) + } + + // Update the narrowest and widest row width for the last row. + minRight = minOf(minRight, left) + maxRight = maxOf(maxRight, left) + + return FitWindowResult(allWindowsFit, outRects, minRight, maxRight) + } + + /** Centers task windows in the center of Overview. */ + private fun centerTaskWindows( + layoutBounds: Rect, + maxBottom: Int, + outWindowRects: List, + layoutConfig: DesktopLayoutConfig, + ) { + if (outWindowRects.isEmpty()) { + return + } + + val currentRowUnionRange = RectF(outWindowRects[0]) + var currentRowY = outWindowRects[0].top + var currentRowFirstItemIndex = 0 + val offsetY = (layoutBounds.bottom - maxBottom) / 2f + val horizontal_padding = + if (outWindowRects.size == 1) 0 else layoutConfig.horizontalPaddingBetweenTasks + + // Batch process to center overview desktop task windows within the same row. + fun batchCenterDesktopTaskWindows(endIndex: Int) { + // Calculate the shift amount required to center the desktop task items. + val rangeCenterX = + (currentRowUnionRange.left + currentRowUnionRange.right + horizontal_padding) / 2f + val currentDiffX = (layoutBounds.centerX() - rangeCenterX).coerceAtLeast(0f) + for (j in currentRowFirstItemIndex until endIndex) { + outWindowRects[j].offset(currentDiffX, offsetY) + } + } + + outWindowRects.forEachIndexed { index, rect -> + if (rect.top != currentRowY) { + // As a new row begins processing, batch-shift the previous row's rects + // and reset its parameters. + batchCenterDesktopTaskWindows(index) + currentRowUnionRange.set(rect) + currentRowY = rect.top + currentRowFirstItemIndex = index + } + + // Extend the range by adding the [rect]'s width and extra in-between items + // spacing. + currentRowUnionRange.right = rect.right + } + + // Post-processing rects in the last row. + batchCenterDesktopTaskWindows(outWindowRects.size) + } + + /** Returns true if all task windows can fit in one row. */ + private fun canFitInOneRow(resultRect: List): Boolean { + if (resultRect.isEmpty()) { + return true + } + + val firstTop = resultRect.first().top + return resultRect.all { it.top == firstTop } + } +} diff --git a/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt new file mode 100644 index 0000000000..e035015070 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.mapper + +import android.view.View.OnClickListener +import com.android.launcher3.Flags.enableDesktopExplodedView +import com.android.launcher3.R +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.apptimer.TaskAppTimerUiState +import com.android.quickstep.task.thumbnail.TaskHeaderUiState +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized + +object TaskUiStateMapper { + + /** + * Converts a [TaskData] object into a [TaskHeaderUiState] for display in the UI. + * + * This function handles different types of [TaskData] and determines the appropriate UI state + * based on the data and provided flags. + * + * @param taskData The [TaskData] to convert. Can be null or a specific subclass. + * @param hasHeader A flag indicating whether the UI should display a header. + * @param clickCloseListener A callback when the close button in the UI is clicked. + * @return A [TaskHeaderUiState] representing the UI state for the given task data. + */ + fun toTaskHeaderState( + taskData: TaskData?, + hasHeader: Boolean, + clickCloseListener: OnClickListener?, + ): TaskHeaderUiState = + when { + taskData !is TaskData.Data -> TaskHeaderUiState.HideHeader + canHeaderBeCreated(taskData, hasHeader, clickCloseListener) -> { + TaskHeaderUiState.ShowHeader( + TaskHeaderUiState.ThumbnailHeader( + // TODO(http://b/353965691): figure out what to do when `icon` or + // `titleDescription` is null. + taskData.icon!!, + taskData.titleDescription!!, + clickCloseListener!!, + ) + ) + } + else -> TaskHeaderUiState.HideHeader + } + + /** + * Converts a [TaskData] object into a [TaskThumbnailUiState] for display in the UI. + * + * This function handles different types of [TaskData] and determines the appropriate UI state + * based on the data and provided flags. + * + * @param taskData The [TaskData] to convert. Can be null or a specific subclass. + * @param isLiveTile A flag indicating whether the task data represents live tile. + * @return A [TaskThumbnailUiState] representing the UI state for the given task data. + */ + fun toTaskThumbnailUiState(taskData: TaskData?): TaskThumbnailUiState = + when { + taskData !is TaskData.Data -> Uninitialized + taskData.isLiveTile -> LiveTile + isBackgroundOnly(taskData) -> BackgroundOnly(taskData.backgroundColor) + isSnapshotSplash(taskData) -> + SnapshotSplash( + Snapshot( + taskData.thumbnailData?.thumbnail!!, + taskData.thumbnailData.rotation, + taskData.backgroundColor, + ), + taskData.icon, + ) + + else -> Uninitialized + } + + private fun isBackgroundOnly(taskData: TaskData.Data) = + taskData.isLocked || taskData.thumbnailData == null + + private fun isSnapshotSplash(taskData: TaskData.Data) = + taskData.thumbnailData?.thumbnail != null && !taskData.isLocked + + private fun canHeaderBeCreated( + taskData: TaskData.Data, + hasHeader: Boolean, + clickCloseListener: OnClickListener?, + ) = + enableDesktopExplodedView() && + hasHeader && + taskData.icon != null && + taskData.titleDescription != null && + clickCloseListener != null + + /** + * Converts a [TaskData] object into a [TaskAppTimerUiState] for displaying an app timer toast + * + * @property taskData The [TaskData] to convert. Can be null or a specific sub-class. + * @property stagePosition the position of this task when shown as a group + * @return a [TaskAppTimerUiState] representing state for the information displayed in the app + * timer toast. + */ + fun toTaskAppTimerUiState( + canShowAppTimer: Boolean, + stagePosition: Int, + taskData: TaskData?, + ): TaskAppTimerUiState = + when { + taskData !is TaskData.Data -> TaskAppTimerUiState.Uninitialized + + !canShowAppTimer || taskData.remainingAppTimerDuration == null -> + TaskAppTimerUiState.NoTimer(taskDescription = taskData.titleDescription) + + else -> + TaskAppTimerUiState.Timer( + taskDescription = taskData.titleDescription, + timeRemaining = taskData.remainingAppTimerDuration, + taskPackageName = taskData.packageName, + accessibilityActionId = + if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) { + R.id.action_digital_wellbeing_bottom_right + } else { + R.id.action_digital_wellbeing_top_left + }, + ) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt new file mode 100644 index 0000000000..4e0e9609d0 --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.viewmodel + +import android.graphics.Rect +import android.util.Size +import com.android.quickstep.recents.domain.model.DesktopLayoutConfig +import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData +import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase + +/** ViewModel used for [com.android.quickstep.views.DesktopTaskView]. */ +class DesktopTaskViewModel(private val organizeDesktopTasksUseCase: OrganizeDesktopTasksUseCase) { + /** Positions for desktop tasks as calculated by [organizeDesktopTasksUseCase] */ + var organizedDesktopTaskPositions = emptyList() + private set + + /** + * Computes new task positions using [organizeDesktopTasksUseCase]. The result is stored in + * [organizedDesktopTaskPositions]. This is used for the exploded desktop view where the usecase + * will scale and translate tasks so that they don't overlap. + * + * @param desktopSize the size available for organizing the tasks. + * @param defaultPositions the tasks and their bounds as they appear on a desktop. + * @param layoutConfig the pre-scaled dimension configuration for the desktop layout. + */ + fun organizeDesktopTasks( + desktopSize: Size, + defaultPositions: List, + layoutConfig: DesktopLayoutConfig, + ) { + organizedDesktopTaskPositions = + organizeDesktopTasksUseCase( + desktopBounds = Rect(0, 0, desktopSize.width, desktopSize.height), + taskBounds = defaultPositions, + layoutConfig = layoutConfig, + ) + } +} diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt new file mode 100644 index 0000000000..ad6024a1cf --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskTileUiState.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.viewmodel + +import android.graphics.drawable.Drawable +import com.android.systemui.shared.recents.model.ThumbnailData +import java.time.Duration + +/** + * This class represents the UI state to be consumed by TaskView, GroupTaskView and DesktopTaskView. + * Data class representing the state of a list of tasks. + * + * This class encapsulates a list of [TaskTileUiState] objects, along with a flag indicating whether + * the data is being used for a live tile display. + * + * @property tasks The list of [TaskTileUiState] objects representing the individual tasks. + * @property isLiveTile Indicates whether this data is intended for a live tile. If `true`, the + * running app will be displayed instead of the thumbnail. + * @property sysUiStatusNavFlags Flags for status bar and navigation bar + */ +data class TaskTileUiState( + val tasks: List, + val hasHeader: Boolean, + val sysUiStatusNavFlags: Int, + val taskOverlayEnabled: Boolean, + val isCentralTask: Boolean, +) + +sealed class TaskData { + abstract val taskId: Int + + /** When no data was found for the TaskId provided */ + data class NoData(override val taskId: Int) : TaskData() + + /** + * This class provides UI information related to a Task (App) to be displayed within a TaskView. + * + * @property taskId Identifier of the task + * @property packageName package name for the task + * @property title App title + * @property titleDescription App content description + * @property icon App icon + * @property thumbnailData Information related to the last snapshot retrieved from the app + * @property backgroundColor The background color of the task. + * @property isLocked Indicates whether the task is locked or not. + * @property isLiveTile Indicates whether the task is shown with a live tile or not. + * @property remainingAppTimerDuration time remaining on the app timer for the application. + */ + data class Data( + override val taskId: Int, + val packageName: String, + val title: String?, + val titleDescription: String?, + val icon: Drawable?, + val thumbnailData: ThumbnailData?, + val backgroundColor: Int, + val isLocked: Boolean, + val isLiveTile: Boolean, + val remainingAppTimerDuration: Duration?, + ) : TaskData() +} diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt new file mode 100644 index 0000000000..8e67c10e6c --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.viewmodel + +import android.annotation.ColorInt +import android.util.Log +import androidx.core.graphics.ColorUtils +import com.android.launcher3.Flags.enableCoroutineThreadingImprovements +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.recents.domain.model.TaskId +import com.android.quickstep.recents.domain.model.TaskModel +import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase +import com.android.quickstep.recents.domain.usecase.GetTaskUseCase +import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase +import com.android.quickstep.recents.domain.usecase.ThumbnailPosition +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.ThumbnailData +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * ViewModel used for [com.android.quickstep.views.TaskView], + * [com.android.quickstep.views.DesktopTaskView] and [com.android.quickstep.views.GroupedTaskView]. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class TaskViewModel( + private val taskViewType: TaskViewType, + recentsViewData: RecentsViewData, + private val getTaskUseCase: GetTaskUseCase, + private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase, + private val isThumbnailValidUseCase: IsThumbnailValidUseCase, + private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase, + dispatcherProvider: DispatcherProvider, +) { + private val taskIds = MutableStateFlow(emptySet()) + + private val isLiveTile = + combine( + taskIds, + recentsViewData.runningTaskIds, + recentsViewData.runningTaskShowScreenshot, + ) { taskIds, runningTaskIds, runningTaskShowScreenshot -> + runningTaskIds == taskIds && !runningTaskShowScreenshot + } + .distinctUntilChanged() + + private val isCentralTask = + combine(taskIds, recentsViewData.centralTaskIds) { taskIds, centralTaskIds -> + taskIds == centralTaskIds + } + .distinctUntilChanged() + + private val taskData = + taskIds.flatMapLatest { ids -> + // Combine Tasks requests + val taskFlows = + ids.map { id -> getTaskUseCase(id).map { taskModel -> id to taskModel } } + val combinedTaskFlows = combine(taskFlows) { taskArray -> taskArray } + combine(combinedTaskFlows, isLiveTile, ::mapToTaskData) + } + + private val taskModels = + taskIds.flatMapLatest { ids -> + // Combine Tasks requests + val taskFlows = + ids.map { id -> + getTaskUseCase(id).distinctUntilChanged().map { taskModel -> id to taskModel } + } + combine(taskFlows) { taskArray -> taskArray } + } + + private val overlayEnabled = + when (taskViewType) { + TaskViewType.SINGLE -> + combine( + recentsViewData.overlayEnabled, + recentsViewData.settledFullyVisibleTaskIds, + ) { isOverlayEnabled, settledFullyVisibleTaskIds -> + isOverlayEnabled && settledFullyVisibleTaskIds.any { it in taskIds.value } + } + .distinctUntilChanged() + else -> flowOf(false) + } + + private val preThreadingImprovedState: Flow = + combine(taskData, overlayEnabled, isCentralTask, ::mapToTaskTile) + + private val threadingImprovedState: Flow = + com.android.launcher3.util.coroutines.combine( + taskModels, + recentsViewData.runningTaskIds, + recentsViewData.runningTaskShowScreenshot, + recentsViewData.overlayEnabled, + recentsViewData.settledFullyVisibleTaskIds, + recentsViewData.centralTaskIds, + ) { + taskModels: Array>, + runningTaskIds: Set, + runningTaskShowScreenshot: Boolean, + isOverlayEnabled: Boolean, + settledFullyVisibleTaskIds: Set, + centralTaskIds: Set -> + val taskIds = taskModels.map { it.first }.toSet() + val isCentralTask = taskIds == centralTaskIds + val overlayEnabled = + when (taskViewType) { + TaskViewType.SINGLE -> { + isOverlayEnabled && settledFullyVisibleTaskIds.any { it in taskIds } + } + else -> false + } + val isLiveTile = runningTaskIds == taskIds && !runningTaskShowScreenshot + val taskData = mapToTaskData(taskModels, isLiveTile) + + mapToTaskTile(taskData, overlayEnabled, isCentralTask) + } + + private val taskTileUiStateFlow = + if (enableCoroutineThreadingImprovements()) threadingImprovedState + else preThreadingImprovedState + + val state: Flow = + taskTileUiStateFlow + .distinctUntilChanged() + .debounce { state -> + // Debouncing only when thumbnails are not present gives the best results. + // This is because thumbnail loading is a decent predictor of there being no more + // emissions to come as they are typically the last emission for a TaskView. + if (state.tasks.any { (it as? TaskData.Data)?.thumbnailData?.thumbnail == null }) { + DEBOUNCE_DELAY_MS + } else { + 0 + } + } + .flowOn(dispatcherProvider.lightweightBackground) + + fun bind(vararg taskId: TaskId) { + taskIds.value = taskId.toSet().also { Log.d(TAG, "bind: $it") } + } + + fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean = + isThumbnailValidUseCase(thumbnail, width, height) + + fun getThumbnailPosition( + thumbnail: ThumbnailData?, + width: Int, + height: Int, + isRtl: Boolean, + ): ThumbnailPosition = + getThumbnailPositionUseCase( + thumbnailData = thumbnail, + width = width, + height = height, + isRtl = isRtl, + ) + + private fun mapToTaskTile( + tasks: List, + overlayEnabled: Boolean, + isCentralTask: Boolean, + ): TaskTileUiState { + val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData + return TaskTileUiState( + tasks = tasks, + hasHeader = taskViewType == TaskViewType.DESKTOP, + sysUiStatusNavFlags = getSysUiStatusNavFlagsUseCase(firstThumbnailData), + taskOverlayEnabled = overlayEnabled, + isCentralTask = isCentralTask, + ) + } + + private fun mapToTaskData( + result: Array>, + isLiveTile: Boolean, + ): List = result.map { mapToTaskData(it.first, it.second, isLiveTile) } + + private fun mapToTaskData(taskId: TaskId, result: TaskModel?, isLiveTile: Boolean): TaskData = + result?.let { + TaskData.Data( + taskId = taskId, + packageName = result.packageName, + title = result.title, + titleDescription = result.titleDescription, + icon = result.icon, + thumbnailData = result.thumbnail, + backgroundColor = result.backgroundColor.removeAlpha(), + isLocked = result.isLocked, + isLiveTile = isLiveTile && !result.isMinimized, + remainingAppTimerDuration = result.remainingAppDuration, + ) + } ?: TaskData.NoData(taskId) + + @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff) + + private companion object { + const val TAG = "TaskViewModel" + const val DEBOUNCE_DELAY_MS = 16L + } +} diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt index 28212cf3a5..803fb354ff 100644 --- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt +++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt @@ -20,8 +20,19 @@ import kotlinx.coroutines.flow.MutableStateFlow // This is far from complete but serves the purpose of enabling refactoring in other areas class RecentsViewData { - val fullscreenProgress = MutableStateFlow(1f) + // Whether the current RecentsView state supports task overlays. + // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM. + val overlayEnabled = MutableStateFlow(false) - // This is typically a View concern but it is used to invalidate rendering in other Views - val scale = MutableStateFlow(1f) + // The settled set of visible taskIds that is updated after RecentsView scroll settles. + val settledFullyVisibleTaskIds = MutableStateFlow(emptySet()) + + // The id for the task ids in the TaskView that controls the Actions View + val centralTaskIds = MutableStateFlow(emptySet()) + + // A list of taskIds that are associated with a RecentsAnimationController. */ + val runningTaskIds = MutableStateFlow(emptySet()) + + // Whether we should use static screenshot instead of live tile for taskIds in [runningTaskIds] + val runningTaskShowScreenshot = MutableStateFlow(false) } diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt new file mode 100644 index 0000000000..ec8736a6ef --- /dev/null +++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt @@ -0,0 +1,81 @@ +/* + * 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.quickstep.recents.viewmodel + +import com.android.quickstep.recents.data.RecentTasksRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first + +class RecentsViewModel( + private val recentsTasksRepository: RecentTasksRepository, + private val recentsViewData: RecentsViewData, + private val displayId: Int, +) { + private var visibleTaskIds = emptySet() + + fun refreshAllTaskData() { + recentsTasksRepository.getAllTaskData(displayId, true) + } + + fun updateVisibleTasks(visibleTaskIdList: List) { + visibleTaskIds = visibleTaskIdList.toSet() + recentsTasksRepository.setVisibleTasks(displayId, visibleTaskIds) + } + + fun updateTasksFullyVisible(taskIds: Set) { + recentsViewData.settledFullyVisibleTaskIds.value = taskIds + } + + fun updateCentralTaskIds(taskIds: Set) { + recentsViewData.centralTaskIds.value = taskIds + } + + fun setOverlayEnabled(isOverlayEnabled: Boolean) { + recentsViewData.overlayEnabled.value = isOverlayEnabled + } + + suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map?) { + val visibleThumbnails = updatedThumbnails?.filterKeys { it in visibleTaskIds } + if (visibleThumbnails.isNullOrEmpty()) return + combine( + visibleThumbnails.map { + recentsTasksRepository.getThumbnailById(it.key).filter { thumbnailData -> + thumbnailData?.snapshotId == it.value.snapshotId + } + } + ) {} + .first() + } + + suspend fun waitForRunningTaskShowScreenshotToUpdate() { + recentsViewData.runningTaskShowScreenshot.filter { it }.first() + } + + fun onReset() { + updateVisibleTasks(emptyList()) + } + + fun updateRunningTask(taskIds: Set) { + recentsViewData.runningTaskIds.value = taskIds + } + + fun setRunningTaskShowScreenshot(showScreenshot: Boolean) { + recentsViewData.runningTaskShowScreenshot.value = showScreenshot + } +} diff --git a/quickstep/src/com/android/quickstep/task/apptimer/DurationFormatter.kt b/quickstep/src/com/android/quickstep/task/apptimer/DurationFormatter.kt new file mode 100644 index 0000000000..0c4a4d8331 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/apptimer/DurationFormatter.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.apptimer + +import android.content.Context +import android.icu.text.MeasureFormat +import android.icu.util.Measure +import android.icu.util.MeasureUnit +import androidx.annotation.StringRes +import java.time.Duration +import java.util.Locale + +/** Formats the given duration as a user friendly text. */ +object DurationFormatter { + fun format( + context: Context, + duration: Duration, + @StringRes durationLessThanOneMinuteStringId: Int, + ): String { + val hours = Math.toIntExact(duration.toHours()) + val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes()) + return when { + // Apply FormatWidth.NARROW if both the hour part and the minute part are non-zero. + hours > 0 && minutes > 0 -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW) + .formatMeasures( + Measure(hours, MeasureUnit.HOUR), + Measure(minutes, MeasureUnit.MINUTE), + ) + // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced). + hours > 0 -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(hours, MeasureUnit.HOUR)) + // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced). + minutes > 0 -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(minutes, MeasureUnit.MINUTE)) + // Use a specific string for usage less than one minute but non-zero. + duration > Duration.ZERO -> context.getString(durationLessThanOneMinuteStringId) + // Otherwise, return 0-minute string. + else -> + MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) + .formatMeasures(Measure(0, MeasureUnit.MINUTE)) + } + } +} diff --git a/quickstep/src/com/android/quickstep/task/apptimer/TaskAppTimerUiState.kt b/quickstep/src/com/android/quickstep/task/apptimer/TaskAppTimerUiState.kt new file mode 100644 index 0000000000..08c92db044 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/apptimer/TaskAppTimerUiState.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.apptimer + +import androidx.annotation.IdRes +import java.time.Duration + +/** + * UI state of the digital wellbeing app timer toast + * + * @property taskDescription description of the task for which the timer is being displayed. + */ +sealed class TaskAppTimerUiState(open val taskDescription: String?) { + /** Timer information not available in UI. */ + data object Uninitialized : TaskAppTimerUiState(null) + + /** No timer information to display */ + data class NoTimer(override val taskDescription: String?) : + TaskAppTimerUiState(taskDescription) + + /** + * Represents the UI state necessary to show an app timer on a task + * + * @property timeRemaining time remaining on the app timer for the application. + * @property taskDescription description of the task for which the timer is being displayed. + * @property taskPackageName package name for of the top component for the task's app. + * @property accessibilityActionId action id to use for tap like accessibility actions on this + * timer. + */ + data class Timer( + val timeRemaining: Duration, + override val taskDescription: String?, + val taskPackageName: String, + @IdRes val accessibilityActionId: Int, + ) : TaskAppTimerUiState(taskDescription) +} diff --git a/quickstep/src/com/android/quickstep/task/apptimer/TimerTextHelper.kt b/quickstep/src/com/android/quickstep/task/apptimer/TimerTextHelper.kt new file mode 100644 index 0000000000..0d0ca841f4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/apptimer/TimerTextHelper.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.apptimer + +import android.content.Context +import com.android.launcher3.R +import com.android.launcher3.Utilities +import java.time.Duration + +/** A helper class that is responsible for building the digital wellbeing timer text. */ +class TimerTextHelper(private val context: Context, timeLeft: Duration) { + val formattedDuration = + DurationFormatter.format(context, timeLeft, R.string.shorter_duration_less_than_one_minute) + + /** Provides the time left as a user friendly text that fits in the [availableWidth]. */ + fun getTextThatFits(availableWidth: Int, textPaint: android.text.TextPaint): CharSequence { + val iconOnlyText = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, "") + + if (availableWidth == 0) { + return iconOnlyText + } + + // "$icon $formattedDuration left today" + val fullText = + Utilities.prefixTextWithIcon( + context, + R.drawable.ic_hourglass_top, + context.getString(R.string.time_left_for_app, formattedDuration), + ) + + val textWidth = textPaint.measureText(fullText, /* start= */ 0, /* end= */ fullText.length) + val textToWidthRatio = textWidth / availableWidth + return when { + textToWidthRatio > ICON_ONLY_WIDTH_RATIO_THRESHOLD -> + // "$icon" + iconOnlyText + + textToWidthRatio > ICON_SHORT_TEXT_WIDTH_RATIO_THRESHOLD -> + // "$icon $formattedDuration" + Utilities.prefixTextWithIcon( + context, + R.drawable.ic_hourglass_top, + formattedDuration, + ) + + else -> fullText + } + } + + companion object { + // If the full text ("$icon $formattedDuration left today") takes a lot more space than is + // available, it is likely short text won't fit either. So, we fallback to just an icon. + private const val ICON_ONLY_WIDTH_RATIO_THRESHOLD = 1.2f + + // If the full text fits but leaves very little space, use short text instead for + // comfortable viewing. + private const val ICON_SHORT_TEXT_WIDTH_RATIO_THRESHOLD = 0.8f + } +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt new file mode 100644 index 0000000000..45b368709a --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt @@ -0,0 +1,46 @@ +/* + * 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.quickstep.task.thumbnail + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.util.AttributeSet +import android.view.View + +class LiveTileView : View { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + override fun onDraw(canvas: Canvas) { + canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), CLEAR_PAINT) + } + + companion object { + private val CLEAR_PAINT = + Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } + } +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt new file mode 100644 index 0000000000..ca8517e6a7 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskContentView.kt @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.content.Context +import android.content.Intent +import android.graphics.Canvas +import android.graphics.Outline +import android.graphics.Path +import android.graphics.Rect +import android.provider.Settings +import android.util.AttributeSet +import android.util.FloatProperty +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import android.view.ViewStub +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.android.launcher3.Flags.enableCursorHoverStates +import com.android.launcher3.Flags.enableRefactorDigitalWellbeingToast +import com.android.launcher3.R +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.MultiPropertyDelegate +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.ViewPool +import com.android.quickstep.DesktopFullscreenDrawParams.Companion.computeCornerRadius +import com.android.quickstep.task.apptimer.TaskAppTimerUiState +import com.android.quickstep.task.apptimer.TimerTextHelper +import com.android.quickstep.util.BorderAnimator +import com.android.quickstep.util.BorderAnimator.Companion.DEFAULT_BORDER_COLOR +import com.android.quickstep.util.BorderAnimator.Companion.DEFAULT_INTERPOLATOR +import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator +import com.android.quickstep.util.setActivityStarterClickListener +import com.android.quickstep.views.TaskHeaderView +import kotlin.math.max + +/** + * TaskContentView is a wrapper around the TaskHeaderView, TaskThumbnailView and Digital wellbeing + * app timer toast. It is a sibling to AiAi (TaskOverlay). + * + * When enableRefactorDigitalWellbeingToast is off, it is sibling to digital wellbeing toast unlike + * when the flag is on. + */ +class TaskContentView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs), ViewPool.Reusable { + + private var taskHeaderView: TaskHeaderView? = null + private var taskThumbnailView: TaskThumbnailView? = null + private var taskAppTimerToast: TextView? = null + + private var timerTextHelper: TimerTextHelper? = null + private var timerUiState: TaskAppTimerUiState = TaskAppTimerUiState.Uninitialized + private var timerUsageAccessibilityAction: AccessibilityAction? = null + private val timerToastHeight = + context.resources.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height) + + private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null + private val outlinePath = Path() + + private val borderWidthPx: Int by lazy { + context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width) + } + + private var activeFocusAnimator: AnimatorSet? = null + private var activeHoverAnimator: AnimatorSet? = null + + private val focusBorderAnimator: BorderAnimator by lazy { + createSimpleBorderAnimator( + borderRadiusPx = computeCornerRadius(context).toInt(), + borderWidthPx = borderWidthPx, + boundsBuilder = { it.set(0, 0, width, height) }, + targetView = this, + borderColor = + context + .obtainStyledAttributes(attrs, R.styleable.TaskContentView) + .getColor(R.styleable.TaskContentView_focusBorderColor, DEFAULT_BORDER_COLOR), + ) + } + + private val hoverBorderAnimator: BorderAnimator by lazy { + createSimpleBorderAnimator( + borderRadiusPx = computeCornerRadius(context).toInt(), + borderWidthPx = borderWidthPx, + boundsBuilder = { it.set(0, 0, width, height) }, + targetView = this, + borderColor = + context + .obtainStyledAttributes(attrs, R.styleable.TaskContentView) + .getColor(R.styleable.TaskContentView_hoverBorderColor, DEFAULT_BORDER_COLOR), + ) + } + + private var hoverBorderVisible = false + set(value) { + if (field == value) { + return + } + + field = value + + activeHoverAnimator?.cancel() + activeHoverAnimator = animateBorder(OUTLINE_EXPANSION_HOVER, hoverBorderAnimator, value) + } + + /** + * Sets the outline bounds of the view. Default to use view's bound as outline when set to null. + */ + var outlineBounds: Rect? = null + set(value) { + field = value + invalidateOutline() + } + + private val bounds = Rect() + + var cornerRadius: Float = 0f + set(value) { + field = value + invalidateOutline() + } + + private var outlineExpansion = 0.0f + set(value) { + field = value + invalidateOutline() + } + + private val outlineExpansionFactory: MultiPropertyFactory = + MultiPropertyFactory(this, OUTLINE_EXPANSION, OutlineExpansion.entries.size) { + a: Float, + b: Float -> + max(a, b) + } + private var outlineExpansionFocus by + MultiPropertyDelegate(outlineExpansionFactory, OutlineExpansion.FOCUS) + private var outlineExpansionHover by + MultiPropertyDelegate(outlineExpansionFactory, OutlineExpansion.HOVER) + + var isHoverable: Boolean = false; + + init { + setWillNotDraw(!enableCursorHoverStates()) + } + + override fun onFinishInflate() { + super.onFinishInflate() + createTaskThumbnailView() + } + + override fun setScaleX(scaleX: Float) { + super.setScaleX(scaleX) + taskThumbnailView?.parentScaleXUpdated(scaleX) + } + + override fun setScaleY(scaleY: Float) { + super.setScaleY(scaleY) + taskThumbnailView?.parentScaleYUpdated(scaleY) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + clipToOutline = true + outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + val outlineRect = outlineBounds ?: bounds + val expansion = outlineExpansion.toInt() + outlinePath.apply { + rewind() + addRoundRect( + outlineRect.left.toFloat() - expansion, + outlineRect.top.toFloat() - expansion, + outlineRect.right.toFloat() + expansion, + outlineRect.bottom.toFloat() + expansion, + (cornerRadius + expansion) / scaleX, + (cornerRadius + expansion) / scaleY, + Path.Direction.CW, + ) + } + outline.setPath(outlinePath) + } + } + } + + override fun onRecycle() { + taskHeaderView?.isInvisible = true + taskHeaderView?.alpha = 1.0f + onSizeChanged = null + outlineBounds = null + alpha = 1.0f + taskThumbnailView?.onRecycle() + taskAppTimerToast?.isInvisible = true + timerUiState = TaskAppTimerUiState.Uninitialized + timerTextHelper = null + timerUsageAccessibilityAction = null + outlineExpansionHover = 0f + outlineExpansionFocus = 0f + hoverBorderVisible = false + } + + fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) { + onSizeChanged = action + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + onSizeChanged?.invoke(width, height) + bounds.set(0, 0, w, h) + updateTimerText(w) + invalidateOutline() + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + if (isFocusable()) { + focusBorderAnimator.drawBorder(canvas) + } + if (isHoverable) { + hoverBorderAnimator.drawBorder(canvas) + } + } + + public override fun onFocusChanged( + gainFocus: Boolean, + direction: Int, + previouslyFocusedRect: Rect?, + ) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + + activeFocusAnimator?.cancel() + activeFocusAnimator = animateBorder(OUTLINE_EXPANSION_FOCUS, focusBorderAnimator, gainFocus) + } + + override fun onHoverEvent(event: MotionEvent): Boolean { + if (enableCursorHoverStates() && isHoverable) { + when (event.action) { + MotionEvent.ACTION_HOVER_ENTER -> { + hoverBorderVisible = true + } + MotionEvent.ACTION_HOVER_EXIT -> { + hoverBorderVisible = false + } + } + } + return super.onHoverEvent(event) + } + + fun onParentAnimationProgress(progress: Float) { + taskAppTimerToast?.apply { translationY = timerToastHeight * (1f - progress) } + } + + /** Returns accessibility actions supported by items in the task content view. */ + fun getSupportedAccessibilityActions(): List { + return listOfNotNull(timerUsageAccessibilityAction) + } + + fun handleAccessibilityAction(action: Int): Boolean { + timerUsageAccessibilityAction?.let { + if (action == it.id) { + return taskAppTimerToast?.callOnClick() ?: false + } + } + + return false + } + + private fun createHeaderView(taskHeaderState: TaskHeaderUiState) { + if (taskHeaderView == null && taskHeaderState is TaskHeaderUiState.ShowHeader) { + taskHeaderView = + findViewById(R.id.task_header_view) + .apply { layoutResource = R.layout.task_header_view } + .inflate() as TaskHeaderView + } + } + + private fun createTaskThumbnailView() { + if (taskThumbnailView == null) { + taskThumbnailView = + findViewById(R.id.snapshot) + .apply { layoutResource = R.layout.task_thumbnail } + .inflate() as TaskThumbnailView + } + } + + private fun createAppTimerToastView(taskAppTimerUiState: TaskAppTimerUiState) { + if ( + enableRefactorDigitalWellbeingToast() && + taskAppTimerToast == null && + taskAppTimerUiState is TaskAppTimerUiState.Timer + ) { + taskAppTimerToast = + findViewById(R.id.task_app_timer_toast) + .apply { layoutResource = R.layout.task_app_timer_toast } + .inflate() as TextView + } + } + + fun setState( + taskHeaderState: TaskHeaderUiState, + taskThumbnailUiState: TaskThumbnailUiState, + taskAppTimerUiState: TaskAppTimerUiState, + taskId: Int?, + ) { + createHeaderView(taskHeaderState) + taskHeaderView?.setState(taskHeaderState) + taskThumbnailView?.setState(taskThumbnailUiState, taskId) + createAppTimerToastView(taskAppTimerUiState) + if (enableRefactorDigitalWellbeingToast() && timerUiState != taskAppTimerUiState) { + setAppTimerToastState(taskAppTimerUiState) + updateContentDescriptionWithTimer(taskAppTimerUiState) + } + } + + fun setTaskHeaderAlpha(alpha: Float) { + taskHeaderView?.alpha = alpha + } + + private fun updateContentDescriptionWithTimer(state: TaskAppTimerUiState) { + taskThumbnailView?.contentDescription = + when (state) { + is TaskAppTimerUiState.Uninitialized -> return + is TaskAppTimerUiState.NoTimer -> state.taskDescription + is TaskAppTimerUiState.Timer -> + timerTextHelper?.let { + context.getString( + R.string.task_contents_description_with_remaining_time, + state.taskDescription, + context.getString(R.string.time_left_for_app, it.formattedDuration), + ) + } + } + } + + private fun setAppTimerToastState(state: TaskAppTimerUiState) { + timerUiState = state + + taskAppTimerToast?.apply { + when (state) { + is TaskAppTimerUiState.Uninitialized -> isInvisible = true + is TaskAppTimerUiState.NoTimer -> isInvisible = true + is TaskAppTimerUiState.Timer -> { + timerTextHelper = TimerTextHelper(context, state.timeRemaining) + isInvisible = false + updateTimerText(width) + + // TODO: add WW logging on the app usage settings click. + setActivityStarterClickListener( + appUsageSettingsIntent(state.taskPackageName), + "app usage settings for task ${state.taskDescription}", + ) + + timerUsageAccessibilityAction = + appUsageSettingsAccessibilityAction( + context, + state.accessibilityActionId, + state.taskDescription, + ) + } + } + } + } + + private fun updateTimerText(width: Int) { + taskAppTimerToast?.apply { + val helper = timerTextHelper + + if (isVisible && helper != null) { + text = helper.getTextThatFits(width, paint) + } + } + } + + private fun animateBorder( + outlineProperty: FloatProperty, + borderAnimator: BorderAnimator, + show: Boolean, + ): AnimatorSet? { + val targetOutlineExpansion = if (show) borderWidthPx.toFloat() else 0f + val outlineExpansionAnimator = + ObjectAnimator.ofFloat(this, outlineProperty, targetOutlineExpansion) + val borderEffectAnimator = borderAnimator.buildAnimator(show) + return AnimatorSet().apply { + playTogether(outlineExpansionAnimator, borderEffectAnimator) + duration = borderEffectAnimator.duration + interpolator = DEFAULT_INTERPOLATOR + start() + } + } + + companion object { + const val TAG = "TaskContentView" + + private fun appUsageSettingsIntent(packageName: String) = + Intent(Intent(Settings.ACTION_APP_USAGE_SETTINGS)) + .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + + private fun appUsageSettingsAccessibilityAction( + context: Context, + @IdRes actionId: Int, + taskDescription: String?, + ) = + AccessibilityAction( + actionId, + context.getString(R.string.split_app_usage_settings, taskDescription), + ) + + private enum class OutlineExpansion { + FOCUS, + HOVER, + } + + private val OUTLINE_EXPANSION: FloatProperty = + KFloatProperty(TaskContentView::outlineExpansion) + private val OUTLINE_EXPANSION_FOCUS: FloatProperty = + KFloatProperty(TaskContentView::outlineExpansionFocus) + private val OUTLINE_EXPANSION_HOVER: FloatProperty = + KFloatProperty(TaskContentView::outlineExpansionHover) + } +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskHeaderUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskHeaderUiState.kt new file mode 100644 index 0000000000..09fb5409b5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskHeaderUiState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail + +import android.graphics.drawable.Drawable +import android.view.View + +sealed class TaskHeaderUiState { + data class ShowHeader(val header: ThumbnailHeader) : TaskHeaderUiState() + + data object HideHeader : TaskHeaderUiState() + + data class ThumbnailHeader( + val icon: Drawable, + val title: String, + val clickCloseListener: View.OnClickListener, + ) +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt index 40f9b28eec..a5c9ac032f 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt @@ -17,18 +17,23 @@ package com.android.quickstep.task.thumbnail import android.graphics.Bitmap -import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.Surface import androidx.annotation.ColorInt sealed class TaskThumbnailUiState { data object Uninitialized : TaskThumbnailUiState() - data object LiveTile : TaskThumbnailUiState() + data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState() + + data object LiveTile : TaskThumbnailUiState() + + data class SnapshotSplash(val snapshot: Snapshot, val splash: Drawable?) : + TaskThumbnailUiState() + data class Snapshot( val bitmap: Bitmap, - val drawnRect: Rect, - @ColorInt val backgroundColor: Int - ) : TaskThumbnailUiState() + @Surface.Rotation val thumbnailRotation: Int, + @ColorInt val backgroundColor: Int, + ) } - -data class TaskThumbnail(val taskId: Int, val isRunning: Boolean) diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt index 2836c892c6..b6928ae67b 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt @@ -17,133 +17,239 @@ package com.android.quickstep.task.thumbnail import android.content.Context -import android.content.res.Configuration -import android.graphics.Canvas import android.graphics.Color +import android.graphics.Matrix import android.graphics.Outline -import android.graphics.Paint -import android.graphics.PorterDuff -import android.graphics.PorterDuffXfermode +import android.graphics.Path import android.graphics.Rect +import android.graphics.drawable.ShapeDrawable import android.util.AttributeSet +import android.util.Log import android.view.View import android.view.ViewOutlineProvider +import android.widget.FrameLayout import androidx.annotation.ColorInt -import com.android.launcher3.Utilities +import androidx.core.view.isInvisible +import com.android.launcher3.Flags.enableRefactorTaskContentView +import com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA +import com.android.launcher3.R +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.ViewPool import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized -import com.android.quickstep.util.TaskCornerRadius -import com.android.quickstep.views.RecentsView -import com.android.quickstep.views.RecentsViewContainer -import com.android.quickstep.views.TaskView -import com.android.systemui.shared.system.QuickStepContract -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch +import com.android.quickstep.views.FixedSizeImageView -class TaskThumbnailView : View { - // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped - // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView] - // This is using a lazy for now because the dependencies cannot be obtained without DI. - val viewModel by lazy { - val recentsView = - RecentsViewContainer.containerFromContext(context) - .getOverviewPanel>() - TaskThumbnailViewModel( - recentsView.mRecentsViewData, - (parent as TaskView).taskViewData, - recentsView.mTasksRepository, - ) +class TaskThumbnailView : FrameLayout, ViewPool.Reusable { + private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) } + private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) } + private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) } + private val splashBackground: View by lazy { findViewById(R.id.splash_background) } + private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) } + private val dimAlpha: MultiPropertyFactory by lazy { + MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf) } + private val outlinePath = Path() + private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null private var uiState: TaskThumbnailUiState = Uninitialized - private var inheritedScale: Float = 1f - private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) - private val _measuredBounds = Rect() - private val measuredBounds: Rect - get() { - _measuredBounds.set(0, 0, measuredWidth, measuredHeight) - return _measuredBounds + /** + * Sets the outline bounds of the view. Default to use view's bound as outline when set to null. + */ + var outlineBounds: Rect? = null + set(value) { + field = value + invalidateOutline() } - private var cornerRadius: Float = TaskCornerRadius.get(context) - private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context) - constructor(context: Context?) : super(context) - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + private val bounds = Rect() + + var cornerRadius: Float = 0f + set(value) { + field = value + invalidateOutline() + } + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor( - context: Context?, + context: Context, attrs: AttributeSet?, defStyleAttr: Int, ) : super(context, attrs, defStyleAttr) override fun onAttachedToWindow() { super.onAttachedToWindow() - // TODO(b/335396935) replace MainScope with shorter lifecycle. - MainScope().launch { - viewModel.uiState.collect { viewModelUiState -> - uiState = viewModelUiState - invalidate() - } + if (enableRefactorTaskContentView()) { + return } - MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } } - MainScope().launch { - viewModel.inheritedScale.collect { viewModelInheritedScale -> - inheritedScale = viewModelInheritedScale - invalidateOutline() - } - } - clipToOutline = true outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect(measuredBounds, getCurrentCornerRadius()) + val outlineRect = outlineBounds ?: bounds + outlinePath.apply { + rewind() + addRoundRect( + outlineRect.left.toFloat(), + outlineRect.top.toFloat(), + outlineRect.right.toFloat(), + outlineRect.bottom.toFloat(), + cornerRadius / scaleX, + cornerRadius / scaleY, + Path.Direction.CW, + ) + } + outline.setPath(outlinePath) } } } - override fun onDraw(canvas: Canvas) { - when (val uiStateVal = uiState) { - is Uninitialized -> drawBackgroundOnly(canvas, Color.BLACK) - is LiveTile -> drawTransparentUiState(canvas) - is Snapshot -> drawSnapshotState(canvas, uiStateVal) - is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor) + override fun onRecycle() { + uiState = Uninitialized + if (!enableRefactorTaskContentView()) { + onSizeChanged = null + outlineBounds = null + } + resetViews() + } + + fun setState(state: TaskThumbnailUiState, taskId: Int? = null) { + if (uiState == state) return + logDebug("taskId: $taskId - uiState changed from: $uiState to: $state") + uiState = state + resetViews() + when (state) { + is Uninitialized -> {} + is LiveTile -> drawLiveWindow() + is SnapshotSplash -> drawSnapshotSplash(state) + is BackgroundOnly -> drawBackground(state.backgroundColor) } } - private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) { - backgroundPaint.color = backgroundColor - canvas.drawRect(measuredBounds, backgroundPaint) + /** + * Updates the alpha of the dim layer on top of this view. If dimAlpha is 0, no dimming is + * applied; if dimAlpha is 1, the thumbnail will be the extracted background color. + * + * @param tintAmount The amount of alpha that will be applied to the dim layer. + */ + fun updateTintAmount(tintAmount: Float) { + dimAlpha[ScrimViewAlpha.TintAmount.ordinal].value = tintAmount } - override fun onConfigurationChanged(newConfig: Configuration?) { - super.onConfigurationChanged(newConfig) + fun updateMenuOpenProgress(progress: Float) { + dimAlpha[ScrimViewAlpha.MenuProgress.ordinal].value = progress * MAX_SCRIM_ALPHA + } - cornerRadius = TaskCornerRadius.get(context) - fullscreenCornerRadius = QuickStepContract.getWindowCornerRadius(context) + fun updateSplashAlpha(value: Float) { + splashBackground.alpha = value + splashIcon.alpha = value + } + + fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) { + if (enableRefactorTaskContentView()) { + return + } + onSizeChanged = action + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + if (enableRefactorTaskContentView()) { + return + } + super.onSizeChanged(w, h, oldw, oldh) + onSizeChanged?.invoke(width, height) + bounds.set(0, 0, w, h) invalidateOutline() } - private fun drawTransparentUiState(canvas: Canvas) { - canvas.drawRect(measuredBounds, CLEAR_PAINT) + override fun setScaleX(scaleX: Float) { + if (enableRefactorTaskContentView()) { + return + } + super.setScaleX(scaleX) + // Splash icon should ignore scale on TTV + splashIcon.scaleX = 1 / scaleX } - private fun drawSnapshotState(canvas: Canvas, snapshot: Snapshot) { - drawBackgroundOnly(canvas, snapshot.backgroundColor) - canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null) + override fun setScaleY(scaleY: Float) { + if (enableRefactorTaskContentView()) { + return + } + super.setScaleY(scaleY) + // Splash icon should ignore scale on TTV + splashIcon.scaleY = 1 / scaleY } - private fun getCurrentCornerRadius() = - Utilities.mapRange( - viewModel.recentsFullscreenProgress.value, - cornerRadius, - fullscreenCornerRadius - ) / inheritedScale + fun parentScaleXUpdated(scaleX: Float) { + // Splash icon should ignore scale on TTV + splashIcon.scaleX = 1 / scaleX + } - companion object { - private val CLEAR_PAINT = - Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } + fun parentScaleYUpdated(scaleY: Float) { + // Splash icon should ignore scale on TTV + splashIcon.scaleY = 1 / scaleY + } + + private fun resetViews() { + liveTileView.isInvisible = true + thumbnailView.isInvisible = true + thumbnailView.setImageBitmap(null) + splashBackground.alpha = 0f + splashBackground.setBackgroundColor(Color.TRANSPARENT) + splashIcon.alpha = 0f + splashIcon.setImageDrawable(null) + scrimView.alpha = 0f + alpha = 1.0f + setBackgroundColor(Color.TRANSPARENT) + } + + private fun drawBackground(@ColorInt background: Int) { + setBackgroundColor(background) + } + + private fun drawLiveWindow() { + liveTileView.isInvisible = false + } + + private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) { + drawSnapshot(snapshotSplash.snapshot) + + splashBackground.setBackgroundColor(snapshotSplash.snapshot.backgroundColor) + val icon = snapshotSplash.splash?.constantState?.newDrawable()?.mutate() ?: ShapeDrawable() + splashIcon.setImageDrawable(icon) + } + + private fun drawSnapshot(snapshot: Snapshot) { + // Always draw the background since the snapshots might be translucent or partially empty + // E.g. reparented tasks from drag-to-dismiss split screen. + drawBackground(snapshot.backgroundColor) + thumbnailView.setImageBitmap(snapshot.bitmap) + thumbnailView.isInvisible = false + } + + fun setImageMatrix(matrix: Matrix) { + if (uiState is SnapshotSplash) { + thumbnailView.imageMatrix = matrix + } + } + + private fun logDebug(message: String) { + Log.d(TAG, "[TaskThumbnailView@${Integer.toHexString(hashCode())}] $message") + } + + private companion object { + const val TAG = "TaskThumbnailView" + private const val MAX_SCRIM_ALPHA = 0.4f + + enum class ScrimViewAlpha { + MenuProgress, + TintAmount, + } } } diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt deleted file mode 100644 index 4511ea714c..0000000000 --- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.quickstep.task.thumbnail - -import android.annotation.ColorInt -import android.graphics.Rect -import androidx.core.graphics.ColorUtils -import com.android.quickstep.recents.data.RecentTasksRepository -import com.android.quickstep.recents.viewmodel.RecentsViewData -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized -import com.android.quickstep.task.viewmodel.TaskViewData -import com.android.systemui.shared.recents.model.Task -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map - -@OptIn(ExperimentalCoroutinesApi::class) -class TaskThumbnailViewModel( - recentsViewData: RecentsViewData, - taskViewData: TaskViewData, - private val tasksRepository: RecentTasksRepository, -) { - private val task = MutableStateFlow>(flowOf(null)) - private var boundTaskIsRunning = false - - val recentsFullscreenProgress = recentsViewData.fullscreenProgress - val inheritedScale = - combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale -> - recentsScale * taskScale - } - val uiState: Flow = - task - .flatMapLatest { taskFlow -> - taskFlow.map { taskVal -> - when { - taskVal == null -> Uninitialized - boundTaskIsRunning -> LiveTile - isBackgroundOnly(taskVal) -> - BackgroundOnly(taskVal.colorBackground.removeAlpha()) - isSnapshotState(taskVal) -> { - val bitmap = taskVal.thumbnail?.thumbnail!! - Snapshot( - bitmap, - Rect(0, 0, bitmap.width, bitmap.height), - taskVal.colorBackground.removeAlpha() - ) - } - else -> Uninitialized - } - } - } - .distinctUntilChanged() - - fun bind(taskThumbnail: TaskThumbnail) { - boundTaskIsRunning = taskThumbnail.isRunning - task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId) - } - - private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null - - private fun isSnapshotState(task: Task): Boolean { - val thumbnailPresent = task.thumbnail?.thumbnail != null - val taskLocked = task.isLocked - - return thumbnailPresent && !taskLocked - } - - @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff) -} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt new file mode 100644 index 0000000000..c45458c74e --- /dev/null +++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt @@ -0,0 +1,24 @@ +/* + * 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.quickstep.task.thumbnail.data + +import com.android.quickstep.TaskIconCache +import com.android.systemui.shared.recents.model.Task + +interface TaskIconDataSource { + suspend fun getIcon(task: Task): TaskIconCache.TaskCacheEntry +} diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt index 55598f0a2d..6e63ea90d5 100644 --- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt +++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt @@ -16,14 +16,9 @@ package com.android.quickstep.task.thumbnail.data -import com.android.launcher3.util.CancellableTask import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData -import java.util.function.Consumer interface TaskThumbnailDataSource { - fun updateThumbnailInBackground( - task: Task, - callback: Consumer - ): CancellableTask? + suspend fun getThumbnail(task: Task): ThumbnailData? } diff --git a/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt b/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt new file mode 100644 index 0000000000..a1ff0ce02d --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt @@ -0,0 +1,71 @@ +/* + * 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.quickstep.util + +import android.content.Context +import android.hardware.input.InputManager +import android.view.InputDevice +import com.android.launcher3.util.Executors +import com.android.launcher3.util.IntSet + +/** Utility class to maintain a list of actively connected trackpad devices */ +class ActiveTrackpadList(ctx: Context, private val updateCallback: Runnable) : + IntSet(), InputManager.InputDeviceListener { + + private val inputManager = ctx.getSystemService(InputManager::class.java)!! + + init { + inputManager.registerInputDeviceListener(this, Executors.UI_HELPER_EXECUTOR.handler) + inputManager.inputDeviceIds.filter(this::isTrackpadDevice).forEach(this::add) + } + + override fun onInputDeviceAdded(deviceId: Int) { + if (isTrackpadDevice(deviceId)) { + // This updates internal TIS state so it needs to also run on the main + // thread. + Executors.MAIN_EXECUTOR.execute { + val wasEmpty = isEmpty + add(deviceId) + if (wasEmpty) update() + } + } + } + + override fun onInputDeviceChanged(deviceId: Int) {} + + override fun onInputDeviceRemoved(deviceId: Int) { + // This updates internal TIS state so it needs to also run on the main thread. + Executors.MAIN_EXECUTOR.execute { + remove(deviceId) + if (isEmpty) update() + } + } + + private fun update() { + updateCallback.run() + } + + fun destroy() { + inputManager.unregisterInputDeviceListener(this) + clear() + } + + /** This is a blocking binder call that should run on a bg thread. */ + private fun isTrackpadDevice(deviceId: Int) = + inputManager.getInputDevice(deviceId)?.sources == + (InputDevice.SOURCE_MOUSE or InputDevice.SOURCE_TOUCHPAD) +} diff --git a/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt new file mode 100644 index 0000000000..239230c123 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.content.Context +import android.content.Intent +import android.os.Trace +import android.view.Display.DEFAULT_DISPLAY +import com.android.launcher3.provider.RestoreDbTask +import com.android.launcher3.util.Executors +import com.android.launcher3.util.LockedUserState +import com.android.quickstep.OverviewComponentObserver +import com.android.quickstep.RecentsAnimationDeviceState +import com.android.systemui.shared.system.ActivityManagerWrapper + +/** Utility class for preloading overview */ +object ActivityPreloadUtil { + + @JvmStatic + fun preloadOverviewForSUWAllSet(ctx: Context) { + preloadOverview(ctx, fromInit = false, forSUWAllSet = true) + } + + @JvmStatic + fun preloadOverviewForTIS(ctx: Context, fromInit: Boolean) { + preloadOverview(ctx, fromInit = fromInit, forSUWAllSet = false) + } + + private fun preloadOverview(ctx: Context, fromInit: Boolean, forSUWAllSet: Boolean) { + Trace.beginSection("preloadOverview(fromInit=$fromInit, forSUWAllSet=$forSUWAllSet)") + + try { + if (!LockedUserState.get(ctx).isUserUnlocked) return + + val deviceState = + RecentsAnimationDeviceState.REPOSITORY_INSTANCE.get(ctx)[ctx.displayId] ?: return + val overviewCompObserver = OverviewComponentObserver.INSTANCE[ctx] + + // Prevent the overview from being started before the real home on first boot + if (deviceState.isButtonNavMode && !overviewCompObserver.isHomeAndOverviewSame) return + + // Preloading while a restore is pending may cause launcher to start the restore too + // early + if ((RestoreDbTask.isPending(ctx) && !forSUWAllSet) || !deviceState.isUserSetupComplete) + return + + // The activity has been created before the initialization of overview service. It is + // usually happens when booting or launcher is the top activity, so we should already + // have the latest state. + if ( + fromInit && + overviewCompObserver.getContainerInterface(DEFAULT_DISPLAY)?.createdContainer != + null + ) + return + + //ActiveGestureProtoLogProxy.logPreloadRecentsAnimation() + val overviewIntent = Intent(overviewCompObserver.overviewIntentIgnoreSysUiState) + Executors.UI_HELPER_EXECUTOR.execute { + ActivityManagerWrapper.getInstance().preloadRecentsActivity(overviewIntent) + } + } finally { + Trace.endSection() + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java index 8e3d44f8d8..3492788eee 100644 --- a/quickstep/src/com/android/quickstep/util/AnimUtils.java +++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java @@ -17,18 +17,34 @@ package com.android.quickstep.util; import static com.android.app.animation.Interpolators.clampToProgress; +import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import android.animation.AnimatorSet; +import android.os.BinderUtils; import android.os.Bundle; +import android.os.IBinder; import android.os.IRemoteCallback; import android.view.animation.Interpolator; +import androidx.annotation.NonNull; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.statemanager.BaseState; +import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.util.RunnableList; +import com.android.launcher3.views.ActivityContext; +import com.android.quickstep.views.RecentsViewContainer; /** * Utility class containing methods to help manage animations, interpolators, and timings. */ public class AnimUtils { + private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350; + /** * Fetches device-specific timings for the Overview > Split animation * (splitscreen initiated from Overview). @@ -59,14 +75,56 @@ public class AnimUtils { } /** - * Returns a IRemoteCallback which completes the provided list as a result + * Synchronizes the timing for the split dismiss animation to the current transition to + * NORMAL (launcher home/workspace) */ - public static IRemoteCallback completeRunnableListCallback(RunnableList list) { + public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager, + @NonNull RecentsViewContainer container, + @NonNull StatsLogManager.LauncherEvent exitReason, + @NonNull SplitAnimationController animationController) { + StateAnimationConfig config = new StateAnimationConfig(); + BaseState startState = stateManager.getState(); + long duration = startState.getTransitionDuration(container, false /*isToState*/); + if (duration == 0) { + // Case where we're in contextual on workspace (NORMAL), which by default has 0 + // transition duration + duration = DURATION_DEFAULT_SPLIT_DISMISS; + } + config.duration = duration; + AnimatorSet stateAnim = stateManager.createAtomicAnimation( + startState, NORMAL, config); + AnimatorSet dismissAnim = animationController + .createPlaceholderDismissAnim(container, exitReason, duration); + stateAnim.play(dismissAnim); + stateManager.setCurrentAnimation(stateAnim, NORMAL); + stateAnim.start(); + } + + /** + * Returns a IRemoteCallback which completes the provided list as a result or when the owner + * is destroyed + */ + public static IRemoteCallback completeRunnableListCallback( + RunnableList list, ActivityContext owner) { + DefaultLifecycleObserver destroyObserver = new DefaultLifecycleObserver() { + @Override + public void onDestroy(@NonNull LifecycleOwner owner) { + list.executeAllAndClear(); + } + }; + MAIN_EXECUTOR.execute(() -> owner.getLifecycle().addObserver(destroyObserver)); + list.add(() -> owner.getLifecycle().removeObserver(destroyObserver)); + return new IRemoteCallback.Stub() { @Override public void sendResult(Bundle bundle) { MAIN_EXECUTOR.execute(list::executeAllAndDestroy); } + + @Override + public IBinder asBinder() { + return BinderUtils.wrapLifecycle(this, owner.getOwnerCleanupSet()); + } }; } diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java index c7c04ed91c..a2e0628da8 100644 --- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java +++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java @@ -17,11 +17,9 @@ package com.android.quickstep.util; import static com.android.app.animation.Interpolators.DECELERATE; import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.Flags.enableGridOnlyOverview; import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY; import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION; -import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.content.Context; import android.graphics.Matrix; @@ -34,18 +32,11 @@ import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.statemanager.StateManager; -import com.android.launcher3.statemanager.StatefulActivity; -import com.android.launcher3.states.StateAnimationConfig; -import com.android.launcher3.touch.AllAppsSwipeController; -import com.android.quickstep.DeviceConfigWrapper; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; import com.android.quickstep.views.RecentsView; -import com.android.quickstep.views.RecentsViewContainer; /** * Controls an animation that can go beyond progress = 1, at which point resistance should be @@ -57,10 +48,7 @@ public class AnimatorControllerWithResistance { private enum RecentsResistanceParams { FROM_APP(0.75f, 0.5f, 1f, false), - FROM_APP_TO_ALL_APPS(1f, 0.6f, 0.8f, false), FROM_APP_TABLET(1f, 0.7f, 1f, true), - FROM_APP_TABLET_GRID_ONLY(1f, 1f, 1f, true), - FROM_APP_TO_ALL_APPS_TABLET(1f, 0.5f, 0.5f, false), FROM_OVERVIEW(1f, 0.75f, 0.5f, false); RecentsResistanceParams(float scaleStartResist, float scaleMaxResist, @@ -157,46 +145,10 @@ public class AnimatorControllerWithResistance { RecentsParams params = new RecentsParams(context, recentsOrientedState, dp, scaleTarget, scaleProperty, translationTarget, translationProperty); PendingAnimation resistAnim = createRecentsResistanceAnim(params); - - // Apply All Apps animation during the resistance animation. - if (recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()) { - RecentsViewContainer container = - recentsOrientedState.getContainerInterface().getCreatedContainer(); - if (container != null) { - RecentsView recentsView = container.getOverviewPanel(); - StateManager> stateManager = - recentsView.getStateManager(); - if (stateManager.isInStableState(LauncherState.BACKGROUND_APP) - && stateManager.isInTransition()) { - - // Calculate the resistance progress threshold where All Apps will trigger. - float threshold = getAllAppsThreshold(context, recentsOrientedState, dp); - - StateAnimationConfig config = new StateAnimationConfig(); - AllAppsSwipeController.applyOverviewToAllAppsAnimConfig(dp, config, threshold); - AnimatorSet allAppsAnimator = stateManager.createAnimationToNewWorkspace( - LauncherState.ALL_APPS, config).getTarget(); - resistAnim.add(allAppsAnimator); - } - } - } - AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController(); return new AnimatorControllerWithResistance(normalController, resistanceController); } - private static float getAllAppsThreshold(Context context, - RecentsOrientedState recentsOrientedState, DeviceProfile dp) { - int transitionDragLength = - recentsOrientedState.getContainerInterface().getSwipeUpDestinationAndLength( - dp, context, TEMP_RECT, - recentsOrientedState.getOrientationHandler()); - float dragLengthFactor = (float) dp.heightPx / transitionDragLength; - // -1s are because 0-1 is reserved for the normal transition. - float threshold = DeviceConfigWrapper.get().getAllAppsOverviewThreshold() / 100f; - return (threshold - 1) / (dragLengthFactor - 1); - } - /** * Creates the resistance animation for {@link #createForRecents}, or can be used separately * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}. @@ -231,7 +183,7 @@ public class AnimatorControllerWithResistance { params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR); float prevScaleRate = (fullscreenScale - params.startScale) - / (params.dp.heightPx - startRect.bottom); + / (params.dp.getDeviceProperties().getHeightPx() - startRect.bottom); // This is what the scale would be at the end of the drag if we didn't apply resistance. float endScale = params.startScale - prevScaleRate * distanceToCover; // Create an interpolator that resists the scale so the scale doesn't get smaller than @@ -304,18 +256,10 @@ public class AnimatorControllerWithResistance { this.scaleProperty = scaleProperty; this.translationTarget = translationTarget; this.translationProperty = translationProperty; - if (dp.isTablet) { - resistanceParams = - recentsOrientedState.getContainerInterface().allowAllAppsFromOverview() - ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS_TABLET - : enableGridOnlyOverview() - ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY - : RecentsResistanceParams.FROM_APP_TABLET; + if (dp.getDeviceProperties().isTablet()) { + resistanceParams = RecentsResistanceParams.FROM_APP_TABLET; } else { - resistanceParams = - recentsOrientedState.getContainerInterface().allowAllAppsFromOverview() - ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS - : RecentsResistanceParams.FROM_APP; + resistanceParams = RecentsResistanceParams.FROM_APP; } } diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java index e878210164..ff407ea7ee 100644 --- a/quickstep/src/com/android/quickstep/util/AppPairsController.java +++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java @@ -22,16 +22,15 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH; import static com.android.launcher3.model.data.AppInfo.PACKAGE_KEY_COMPARATOR; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SUPPORTS_MULTI_INSTANCE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_NONE; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition; +import static com.android.systemui.shared.recents.utilities.Utilities.isFreeformTask; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE; +import static com.android.wm.shell.shared.split.SplitScreenConstants.getIndex; +import static com.android.wm.shell.shared.split.SplitScreenConstants.isPersistentSnapPosition; import static java.util.stream.Collectors.toList; @@ -45,14 +44,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.app.displaylib.PerDisplayRepository; import com.android.internal.jank.Cuj; +import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherSettings; -import com.android.launcher3.R; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.apppairs.AppPairIcon; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.IconCache; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; @@ -60,24 +58,31 @@ import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.model.data.TaskViewItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; import com.android.launcher3.views.ActivityContext; +import com.android.quickstep.OverviewComponentObserver; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.fallback.window.RecentsWindowFlags; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.views.GroupedTaskView; +import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; /** * Controller class that handles app pair interactions: saving, modifying, deleting, etc. @@ -94,10 +99,10 @@ public class AppPairsController { private static final int BITMASK_SIZE = 16; private static final int BITMASK_FOR_SNAP_POSITION = (1 << BITMASK_SIZE) - 1; - private Context mContext; + private ActivityContext mContext; private final SplitSelectStateController mSplitSelectStateController; private final StatsLogManager mStatsLogManager; - public AppPairsController(Context context, + public AppPairsController(ActivityContext context, SplitSelectStateController splitSelectStateController, StatsLogManager statsLogManager) { mContext = context; @@ -109,6 +114,12 @@ public class AppPairsController { mContext = null; } + private Launcher getLauncher() { + return mContext == null || !OverviewComponentObserver.INSTANCE.get(mContext.asContext()) + .isHomeAndOverviewSame() + ? null : Launcher.ACTIVITY_TRACKER.getCreatedContext(); + } + /** * Returns whether the specified GroupedTaskView can be saved as an app pair. */ @@ -129,29 +140,27 @@ public class AppPairsController { .anyMatch(att -> att != null && att.getItemInfo() != null && ((att.getItemInfo().runtimeStatusFlags & ItemInfoWithIcon.FLAG_NOT_PINNABLE) != 0)); - if (!FeatureFlags.enableAppPairs() - || !taskView.containsMultipleTasks() + if (!taskView.containsMultipleTasks() || hasUnpinnableApp - || !(taskView instanceof GroupedTaskView)) { + || !(taskView instanceof GroupedTaskView groupedTaskView)) { return false; } - GroupedTaskView gtv = (GroupedTaskView) taskView; - List containers = gtv.getTaskContainers(); - ComponentKey taskKey1 = TaskUtils.getLaunchComponentKeyForTask( - containers.get(0).getTask().key); - ComponentKey taskKey2 = TaskUtils.getLaunchComponentKeyForTask( - containers.get(1).getTask().key); - AppInfo app1 = resolveAppInfoByComponent(taskKey1); - AppInfo app2 = resolveAppInfoByComponent(taskKey2); + ComponentKey leftTopComponentKey = TaskUtils.getLaunchComponentKeyForTask( + groupedTaskView.getLeftTopTaskContainer().getTask().key); + ComponentKey rightBottomComponentKey = TaskUtils.getLaunchComponentKeyForTask( + groupedTaskView.getRightBottomTaskContainer().getTask().key); + AppInfo leftTopAppInfo = resolveAppInfoByComponent(leftTopComponentKey); + AppInfo rightBottomAppInfo = resolveAppInfoByComponent(rightBottomComponentKey); - if (app1 == null || app2 == null) { + if (leftTopAppInfo == null || rightBottomAppInfo == null) { // Disallow saving app pairs for apps that don't have a front-door in Launcher return false; } - if (PackageManagerHelper.isSameAppForMultiInstance(app1, app2)) { - if (!app1.supportsMultiInstance() || !app2.supportsMultiInstance()) { + if (PackageManagerHelper.isSameAppForMultiInstance(leftTopAppInfo, rightBottomAppInfo)) { + if (!leftTopAppInfo.supportsMultiInstance() + || !rightBottomAppInfo.supportsMultiInstance()) { return false; } } @@ -174,25 +183,11 @@ public class AppPairsController { */ public void saveAppPair(GroupedTaskView gtv) { InteractionJankMonitorWrapper.begin(gtv, Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR); - List containers = gtv.getTaskContainers(); - WorkspaceItemInfo recentsInfo1 = containers.get(0).getItemInfo(); - WorkspaceItemInfo recentsInfo2 = containers.get(1).getItemInfo(); - WorkspaceItemInfo app1 = resolveAppPairWorkspaceInfo(recentsInfo1); - WorkspaceItemInfo app2 = resolveAppPairWorkspaceInfo(recentsInfo2); - - if (app1 == null || app2 == null) { - // This shouldn't happen if canSaveAppPair() is called above, but log an error and do - // not create the app pair if the workspace items can't be resolved - Log.w(TAG, "Failed to save app pair due to invalid apps (" - + "app1=" + recentsInfo1.getComponentKey().componentName - + " app2=" + recentsInfo2.getComponentKey().componentName + ")"); - return; - } @PersistentSnapPosition int snapPosition = gtv.getSnapPosition(); if (snapPosition == SNAP_TO_NONE) { // Free snap mode is enabled, just save it as 50/50 split. - snapPosition = SNAP_TO_50_50; + snapPosition = SNAP_TO_2_50_50; } if (!isPersistentSnapPosition(snapPosition)) { // If we received an illegal snap position, log an error and do not create the app pair @@ -201,31 +196,59 @@ public class AppPairsController { return; } - app1.rank = encodeRank(SPLIT_POSITION_TOP_OR_LEFT, snapPosition); - app2.rank = encodeRank(SPLIT_POSITION_BOTTOM_OR_RIGHT, snapPosition); - AppPairInfo newAppPair = new AppPairInfo(app1, app2); + List recentsInfos = + gtv.getTaskContainers().stream().map(TaskContainer::getItemInfo).toList(); + List apps = + recentsInfos.stream().map(this::resolveAppPairWorkspaceInfo).toList(); - IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache(); + if (apps.stream().anyMatch(Objects::isNull)) { + // This shouldn't happen if canSaveAppPair() is called above, but log an error and do + // not create the app pair if the workspace items can't be resolved + StringBuilder error = + new StringBuilder("Failed to save app pair due to invalid apps ("); + for (int i = 0; i < recentsInfos.size(); i++) { + error.append("app").append(i).append("=") + .append(recentsInfos.get(i).getComponentKey().componentName).append(" "); + } + error.append(")"); + Log.w(TAG, error.toString()); + return; + } + + for (int i = 0; i < apps.size(); i++) { + apps.get(i).rank = encodeRank(getIndex(i), snapPosition); + } + AppPairInfo newAppPair = new AppPairInfo(apps); + + IconCache iconCache = LauncherAppState.getInstance(mContext.asContext()).getIconCache(); MODEL_EXECUTOR.execute(() -> { newAppPair.getAppContents().forEach(member -> { member.title = ""; member.bitmap = iconCache.getDefaultIcon(newAppPair.user); - iconCache.getTitleAndIcon(member, member.usingLowResIcon()); + iconCache.getTitleAndIcon(member, member.getMatchingLookupFlag()); }); MAIN_EXECUTOR.execute(() -> { - LauncherAccessibilityDelegate delegate = - QuickstepLauncher.getLauncher(mContext).getAccessibilityDelegate(); - if (delegate != null) { - delegate.addToWorkspace(newAppPair, true, (success) -> { - if (success) { - InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR); - } else { - InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR); - } - }); - mStatsLogManager.logger().withItemInfo(newAppPair) - .log(StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_SAVE); + Launcher launcher = getLauncher(); + LauncherAccessibilityDelegate delegate = launcher == null + ? null : launcher.getAccessibilityDelegate(); + if (delegate == null) { + return; } + if (RecentsWindowFlags.enableLauncherOverviewInWindow.isTrue()) { + PerDisplayRepository recentsWindowManagerRepository = + RecentsWindowManager.REPOSITORY_INSTANCE.get(mContext.asContext()); + recentsWindowManagerRepository.get(gtv.getDisplayId()) + .getStateManager().moveToRestState(); + } + delegate.addToWorkspace(newAppPair, true, (success) -> { + if (success) { + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR); + } else { + InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR); + } + }); + mStatsLogManager.logger().withItemInfo(newAppPair) + .log(StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_SAVE); }); }); } @@ -236,14 +259,16 @@ public class AppPairsController { * * @param cuj Should be an integer from {@link Cuj} or -1 if no CUJ needs to be logged for jank * monitoring + * @param callback Called after the app pair launch finishes animating, or null if no method is + * to be called */ - public void launchAppPair(AppPairIcon appPairIcon, int cuj) { + public void launchAppPair(AppPairIcon appPairIcon, int cuj, + @Nullable Consumer callback) { WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp(); WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp(); ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user); ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user); - mSplitSelectStateController.setLaunchingCuj(cuj); - InteractionJankMonitorWrapper.begin(appPairIcon, cuj); + mSplitSelectStateController.setLaunchingCuj(appPairIcon, cuj); mSplitSelectStateController.findLastActiveTasksAndRunCallback( Arrays.asList(app1Key, app2Key), @@ -277,19 +302,29 @@ public class AppPairsController { mSplitSelectStateController.setLaunchingIconView(appPairIcon); mSplitSelectStateController.launchSplitTasks( - AppPairsController.convertRankToSnapPosition(app1.rank)); + AppPairsController.convertRankToSnapPosition(app1.rank), callback); } ); } + /** + * Launches an app pair but does not specify a callback + */ + public void launchAppPair(AppPairIcon appPairIcon, int cuj) { + launchAppPair(appPairIcon, cuj, null); + } + /** * Returns an AppInfo associated with the app for the given ComponentKey, or null if no such * package exists in the AllAppsStore. */ @Nullable private AppInfo resolveAppInfoByComponent(@NonNull ComponentKey key) { - AllAppsStore appsStore = ActivityContext.lookupContext(mContext) - .getAppsView().getAppsStore(); + Launcher launcher = getLauncher(); + if (launcher == null) { + return null; + } + AllAppsStore appsStore = launcher.getAppsView().getAppsStore(); // First look up the app info in order of: // - The exact activity for the recent task @@ -314,7 +349,7 @@ public class AppPairsController { if (appInfo == null) { return null; } - return appInfo.makeWorkspaceItem(mContext); + return appInfo.makeWorkspaceItem(mContext.asContext()); } /** @@ -327,10 +362,12 @@ public class AppPairsController { * c) App B is on-screen, but App A isn't. * d) Neither is on-screen. * - * If the user tapped an app pair while inside a single app, there are 3 cases: - * a) The on-screen app is App A of the app pair. - * b) The on-screen app is App B of the app pair. - * c) It is neither. + * If the user tapped an app pair while a fullscreen or freeform app is visible on screen, + * there are 4 cases: + * a) At least one of the apps in the app pair is in freeform windowing mode. + * b) The on-screen app is App A of the app pair. + * c) The on-screen app is App B of the app pair. + * d) It is neither. * * For each case, we call the appropriate animation and split launch type. */ @@ -338,7 +375,7 @@ public class AppPairsController { List itemInfos) { TaskbarActivityContext context = (TaskbarActivityContext) launchingIconView.getContext(); List componentKeys = - itemInfos.stream().map(ItemInfo::getComponentKey).collect(toList()); + itemInfos.stream().map(ItemInfo::getComponentKey).toList(); // Use TopTaskTracker to find the currently running app (or apps) TopTaskTracker topTaskTracker = getTopTaskTracker(); @@ -364,7 +401,7 @@ public class AppPairsController { } else { return INVALID_TASK_ID; } - }).collect(toList()); + }).toList(); if (lastActiveTasksOfAppPair.contains(runningTaskId1) && lastActiveTasksOfAppPair.contains(runningTaskId2)) { @@ -403,8 +440,8 @@ public class AppPairsController { ); } else { // Tapped an app pair while in a single app - int runningTaskId = topTaskTracker - .getCachedTopTask(false /* filterOnlyVisibleRecents */).getTaskId(); + final TopTaskTracker.CachedTaskInfo runningTask = topTaskTracker + .getCachedTopTask(false /* filterOnlyVisibleRecents */, context.getDisplayId()); mSplitSelectStateController.findLastActiveTasksAndRunCallback( componentKeys, @@ -412,10 +449,29 @@ public class AppPairsController { foundTasks -> { Task foundTask1 = foundTasks[0]; Task foundTask2 = foundTasks[1]; - boolean task1IsOnScreen = - foundTask1 != null && foundTask1.getKey().getId() == runningTaskId; - boolean task2IsOnScreen = - foundTask2 != null && foundTask2.getKey().getId() == runningTaskId; + + if (DesktopModeStatus.canEnterDesktopMode(context) && (isFreeformTask( + foundTask1) || isFreeformTask(foundTask2))) { + launchAppPair(launchingIconView, + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR); + return; + } + + boolean task1IsOnScreen; + boolean task2IsOnScreen; + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + task1IsOnScreen = foundTask1 != null + && runningTask.topGroupedTaskContainsTask( + foundTask1.getKey().getId()); + task2IsOnScreen = foundTask2 != null + && runningTask.topGroupedTaskContainsTask( + foundTask2.getKey().getId()); + } else { + task1IsOnScreen = foundTask1 != null && foundTask1.getKey().getId() + == runningTask.getTaskId(); + task2IsOnScreen = foundTask2 != null && foundTask2.getKey().getId() + == runningTask.getTaskId(); + } if (!task1IsOnScreen && !task2IsOnScreen) { // Neither App A nor App B are on-screen, launch the app pair normally. @@ -515,6 +571,6 @@ public class AppPairsController { */ @VisibleForTesting public TopTaskTracker getTopTaskTracker() { - return TopTaskTracker.INSTANCE.get(mContext); + return TopTaskTracker.INSTANCE.get(mContext.asContext()); } } diff --git a/quickstep/src/com/android/quickstep/util/AssistContentRequester.java b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java index 0ce54ad2e6..2e3dee6e09 100644 --- a/quickstep/src/com/android/quickstep/util/AssistContentRequester.java +++ b/quickstep/src/com/android/quickstep/util/AssistContentRequester.java @@ -81,7 +81,7 @@ public class AssistContentRequester { try { mActivityTaskManager.requestAssistDataForTask( new AssistDataReceiver(callback, this), taskId, mPackageName, - mAttributionTag); + mAttributionTag, false /* fetchStructure */); } catch (RemoteException e) { Log.e(TAG, "Requesting assist content failed for task: " + taskId, e); } diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java deleted file mode 100644 index 7acb28da11..0000000000 --- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.util; - -import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; - -import com.android.launcher3.R; -import com.android.launcher3.util.MainThreadInitializedObject; -import com.android.launcher3.util.ResourceBasedOverride; -import com.android.launcher3.util.SafeCloseable; - -import java.io.PrintWriter; -import java.util.Optional; - -/** Class to manage Assistant states. */ -public class AssistStateManager implements ResourceBasedOverride, SafeCloseable { - - public static final MainThreadInitializedObject INSTANCE = - forOverride(AssistStateManager.class, R.string.assist_state_manager_class); - - public AssistStateManager() {} - - /** Return {@code true} if the Settings toggle is enabled. */ - public boolean isSettingsAllEntrypointsEnabled() { - return false; - } - - /** Whether search supports showing on the lockscreen. */ - public boolean supportsShowWhenLocked() { - return false; - } - - /** Whether ContextualSearchService invocation path is available. */ - public boolean isContextualSearchServiceAvailable() { - return false; - } - - /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */ - public Optional getLPNHDurationMillis() { - return Optional.empty(); - } - - /** - * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant. - */ - public Optional getLPNHCustomSlopMultiplier() { - return Optional.empty(); - } - - /** Get the Launcher overridden long press home duration to trigger Assistant. */ - public Optional getLPHDurationMillis() { - return Optional.empty(); - } - - /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */ - public Optional getLPHCustomSlopMultiplier() { - return Optional.empty(); - } - - /** Get the long press duration data source. */ - public int getDurationDataSource() { - return 0; - } - - /** Get the long press touch slop multiplier data source. */ - public int getSlopDataSource() { - return 0; - } - - /** Get the haptic bit overridden by AGSA. */ - public Optional getShouldPlayHapticOverride() { - return Optional.empty(); - } - - /** Dump states. */ - public void dump(String prefix, PrintWriter writer) {} - - @Override - public void close() {} -} diff --git a/quickstep/src/com/android/quickstep/util/AssistUtils.java b/quickstep/src/com/android/quickstep/util/AssistUtils.java deleted file mode 100644 index 11b6ea7d21..0000000000 --- a/quickstep/src/com/android/quickstep/util/AssistUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.util; - -import android.content.Context; - -import com.android.launcher3.R; -import com.android.launcher3.util.ResourceBasedOverride; - -/** Utilities to work with Assistant functionality. */ -public class AssistUtils implements ResourceBasedOverride { - - public AssistUtils() {} - - /** Creates AssistUtils as specified by overrides */ - public static AssistUtils newInstance(Context context) { - return Overrides.getObject(AssistUtils.class, context, R.string.assist_utils_class); - } - - /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ - public int[] getSysUiAssistOverrideInvocationTypes() { - return new int[0]; - } - - /** - * @return {@code true} if the override was handled, i.e. an assist surface was shown or the - * request should be ignored. {@code false} means the caller should start assist another way. - */ - public boolean tryStartAssistOverride(int invocationType) { - return false; - } -} diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java new file mode 100644 index 0000000000..207e4825b3 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static android.content.Intent.ACTION_TIMEZONE_CHANGED; +import static android.content.Intent.ACTION_TIME_CHANGED; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.util.ArrayMap; +import android.widget.TextClock.ClockEventDelegate; + +import androidx.annotation.WorkerThread; + +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.util.DaggerSingletonObject; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.SafeCloseable; +import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.util.SettingsCache.OnChangeListener; +import com.android.launcher3.util.SimpleBroadcastReceiver; +import com.android.quickstep.dagger.QuickstepBaseAppComponent; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +/** + * Extension of {@link ClockEventDelegate} to support async event registration + */ +@LauncherAppSingleton +public class AsyncClockEventDelegate extends ClockEventDelegate + implements OnChangeListener, SafeCloseable { + + public static final DaggerSingletonObject INSTANCE = + new DaggerSingletonObject<>(QuickstepBaseAppComponent::getAsyncClockEventDelegate); + + private final Context mContext; + private final SettingsCache mSettingsCache; + private final SimpleBroadcastReceiver mReceiver; + + private final ArrayMap mTimeEventReceivers = new ArrayMap<>(); + private final List mFormatObservers = new ArrayList<>(); + private final Uri mFormatUri = Settings.System.getUriFor(Settings.System.TIME_12_24); + + private boolean mFormatRegistered = false; + private boolean mDestroyed = false; + + @Inject + AsyncClockEventDelegate(@ApplicationContext Context context, + DaggerSingletonTracker tracker, + SettingsCache settingsCache) { + super(context); + mContext = context; + mSettingsCache = settingsCache; + mReceiver = new SimpleBroadcastReceiver( + context, UI_HELPER_EXECUTOR, this::onClockEventReceived); + mReceiver.register(ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED); + tracker.addCloseable(this); + } + + @Override + public void registerTimeChangeReceiver(BroadcastReceiver receiver, Handler handler) { + synchronized (mTimeEventReceivers) { + mTimeEventReceivers.put(receiver, handler == null ? new Handler() : handler); + } + } + + @Override + public void unregisterTimeChangeReceiver(BroadcastReceiver receiver) { + synchronized (mTimeEventReceivers) { + mTimeEventReceivers.remove(receiver); + } + } + + @Override + public void registerFormatChangeObserver(ContentObserver observer, int userHandle) { + if (mDestroyed) { + return; + } + synchronized (mFormatObservers) { + if (!mFormatRegistered && !mDestroyed) { + mSettingsCache.register(mFormatUri, this); + mFormatRegistered = true; + } + mFormatObservers.add(observer); + } + } + + @Override + public void unregisterFormatChangeObserver(ContentObserver observer) { + synchronized (mFormatObservers) { + mFormatObservers.remove(observer); + } + } + + @Override + public void onSettingsChanged(boolean isEnabled) { + if (mDestroyed) { + return; + } + synchronized (mFormatObservers) { + mFormatObservers.forEach(o -> o.dispatchChange(false, mFormatUri)); + } + } + @WorkerThread + private void onClockEventReceived(Intent intent) { + if (mDestroyed) { + return; + } + synchronized (mReceiver) { + mTimeEventReceivers.forEach((r, h) -> h.post(() -> r.onReceive(mContext, intent))); + } + } + + @Override + public void close() { + mDestroyed = true; + mSettingsCache.unregister(mFormatUri, this); + mReceiver.unregisterReceiverSafely(); + } +} diff --git a/quickstep/src/com/android/quickstep/util/BackAnimState.kt b/quickstep/src/com/android/quickstep/util/BackAnimState.kt new file mode 100644 index 0000000000..18cc98cd68 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/BackAnimState.kt @@ -0,0 +1,83 @@ +/* + * 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.quickstep.util + +import android.animation.AnimatorSet +import android.content.Context +import com.android.launcher3.Flags +import com.android.launcher3.Launcher +import com.android.launcher3.LauncherAnimationRunner.AnimationResult +import com.android.launcher3.LauncherState +import com.android.launcher3.anim.AnimatorListeners.forEndCallback +import com.android.launcher3.statemanager.StateManager +import com.android.launcher3.util.RunnableList + +/** Interface to represent animation for back to Launcher transition */ +interface BackAnimState { + + fun addOnAnimCompleteCallback(r: Runnable) + + fun applyToAnimationResult(result: AnimationResult, c: Context) + + fun start(stateManager: StateManager) +} + +class AnimatorBackState(private val springAnim: RectFSpringAnim?, private val anim: AnimatorSet?) : + BackAnimState { + + override fun addOnAnimCompleteCallback(r: Runnable) { + val animWait = RunnableList() + if (Flags.predictiveBackToHomePolish()) { + springAnim?.addAnimatorListener(forEndCallback(animWait::executeAllAndDestroy)) + ?: anim?.addListener(forEndCallback(animWait::executeAllAndDestroy)) + ?: animWait.executeAllAndDestroy() + } else { + val springAnimWait = RunnableList() + springAnim?.addAnimatorListener(forEndCallback(springAnimWait::executeAllAndDestroy)) + ?: springAnimWait.executeAllAndDestroy() + + anim?.addListener( + forEndCallback(Runnable { springAnimWait.add(animWait::executeAllAndDestroy) }) + ) ?: springAnimWait.add(animWait::executeAllAndDestroy) + } + animWait.add(r) + } + + override fun applyToAnimationResult(result: AnimationResult, c: Context) { + result.setAnimation(anim, c) + } + + override fun start(stateManager: StateManager) { + if (anim != null) { + stateManager.setCurrentAnimation(anim) + } + anim?.start() + } +} + +class AlreadyStartedBackAnimState(private val onEndCallback: RunnableList) : BackAnimState { + + override fun addOnAnimCompleteCallback(r: Runnable) { + onEndCallback.add(r) + } + + override fun applyToAnimationResult(result: AnimationResult, c: Context) { + addOnAnimCompleteCallback(result::onAnimationFinished) + } + + override fun start(stateManager: StateManager) {} +} diff --git a/quickstep/src/com/android/quickstep/util/BaseDepthController.java b/quickstep/src/com/android/quickstep/util/BaseDepthController.java index 24403e588c..97f2052626 100644 --- a/quickstep/src/com/android/quickstep/util/BaseDepthController.java +++ b/quickstep/src/com/android/quickstep/util/BaseDepthController.java @@ -15,18 +15,36 @@ */ package com.android.quickstep.util; +import static android.os.Trace.TRACE_TAG_APP; + +import static com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import android.app.WallpaperManager; +import android.graphics.RenderEffect; +import android.graphics.Shader; +import android.gui.EarlyWakeupInfo; +import android.os.Binder; import android.os.IBinder; +import android.os.Trace; import android.util.FloatProperty; import android.util.Log; import android.view.AttachedSurfaceControl; +import android.view.CrossWindowBlurListeners; import android.view.SurfaceControl; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.app.animation.Interpolators; +import com.android.launcher3.Flags; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.systemui.shared.system.BlurUtils; @@ -39,17 +57,18 @@ public class BaseDepthController { public static final float DEPTH_60_PERCENT = 0.6f; public static final float DEPTH_70_PERCENT = 0.7f; - private static final FloatProperty DEPTH = new FloatProperty("depth") { - @Override - public void setValue(BaseDepthController depthController, float depth) { - depthController.setDepth(depth); - } + private static final FloatProperty DEPTH = + new FloatProperty("depth") { + @Override + public void setValue(BaseDepthController depthController, float depth) { + depthController.setDepth(depth); + } - @Override - public Float get(BaseDepthController depthController) { - return depthController.mDepth; - } - }; + @Override + public Float get(BaseDepthController depthController) { + return depthController.mDepth; + } + }; private static final int DEPTH_INDEX_STATE_TRANSITION = 0; private static final int DEPTH_INDEX_WIDGET = 1; @@ -58,7 +77,7 @@ public class BaseDepthController { // b/291401432 private static final String TAG = "BaseDepthController"; - protected final Launcher mLauncher; + protected final QuickstepLauncher mLauncher; /** Property to set the depth for state transition. */ public final MultiProperty stateDepth; /** Property to set the depth for widget picker. */ @@ -73,27 +92,24 @@ public class BaseDepthController { /** * Ratio from 0 to 1, where 0 is fully zoomed out, and 1 is zoomed in. - * + * * @see android.service.wallpaper.WallpaperService.Engine#onZoomChanged(float) */ private float mDepth; - protected SurfaceControl mSurface; + protected SurfaceControl mBaseSurface; - // Hints that there is potentially content behind Launcher and that we shouldn't - // optimize by - // marking the launcher surface as opaque. Only used in certain Launcher states. + protected SurfaceControl mBaseSurfaceOverride; + + // Hints that there is potentially content behind Launcher and that we shouldn't optimize by + // marking the launcher surface as opaque. Only used in certain Launcher states. private boolean mHasContentBehindLauncher; - /** - * Pause blur but allow transparent, can be used when launch something behind - * the Launcher. - */ + /** Pause blur but allow transparent, can be used when launch something behind the Launcher. */ protected boolean mPauseBlurs; /** * Last blur value, in pixels, that was applied. - * For debugging purposes. */ protected int mCurrentBlur; /** @@ -103,19 +119,46 @@ public class BaseDepthController { protected boolean mWaitingOnSurfaceValidity; - public BaseDepthController(Launcher activity) { + private SurfaceControl mBlurSurface = null; + /** + * Info for early wakeup requests to SurfaceFlinger. + */ + private EarlyWakeupInfo mEarlyWakeupInfo = new EarlyWakeupInfo(); + + public BaseDepthController(QuickstepLauncher activity) { mLauncher = activity; - mMaxBlurRadius = activity.getResources().getInteger(R.integer.max_depth_blur_radius); + if (Flags.allAppsBlur() || enableOverviewBackgroundWallpaperBlur()) { + mCrossWindowBlursEnabled = + CrossWindowBlurListeners.getInstance().isCrossWindowBlurEnabled(); + mMaxBlurRadius = activity.getResources().getDimensionPixelSize( + R.dimen.max_depth_blur_radius_enhanced); + } else { + mMaxBlurRadius = activity.getResources().getInteger(R.integer.max_depth_blur_radius); + } mWallpaperManager = activity.getSystemService(WallpaperManager.class); - MultiPropertyFactory depthProperty = new MultiPropertyFactory<>(this, DEPTH, - DEPTH_INDEX_COUNT, Float::max); + MultiPropertyFactory depthProperty = + new MultiPropertyFactory<>(this, DEPTH, DEPTH_INDEX_COUNT, Float::max); stateDepth = depthProperty.get(DEPTH_INDEX_STATE_TRANSITION); widgetDepth = depthProperty.get(DEPTH_INDEX_WIDGET); + mEarlyWakeupInfo.token = new Binder(); + mEarlyWakeupInfo.trace = BaseDepthController.class.getName(); + } + + /** + * Returns if cross window blurs are enabled. In other words, whether launcher should use blurs + * style UI or fallback style UI. + */ + public boolean isCrossWindowBlursEnabled() { + return mCrossWindowBlursEnabled; } protected void setCrossWindowBlursEnabled(boolean isEnabled) { + if (mCrossWindowBlursEnabled == isEnabled) { + return; + } mCrossWindowBlursEnabled = isEnabled; + mLauncher.updateBlurStyle(); applyDepthAndBlur(); } @@ -124,24 +167,42 @@ public class BaseDepthController { } public void pauseBlursOnWindows(boolean pause) { - if (pause != mPauseBlurs) { - mPauseBlurs = pause; - applyDepthAndBlur(); + if (mPauseBlurs == pause) { + return; + } + mPauseBlurs = pause; + applyDepthAndBlur(); + } + + protected void onInvalidSurface() { } + + protected void applyDepthAndBlur() { + applyDepthAndBlur(null, /* applyImmediately */ false, /* skipSimilarBlur */ true); + } + + /** + * Applies depth and blur to the launcher. + * + * @param transaction optional Surface to apply to the blur to. + * @param applyImmediately whether to apply the blur immediately or defer to the next frame. + */ + protected void applyDepthAndBlur(SurfaceControl.Transaction transaction, + boolean applyImmediately, boolean skipSimilarBlur) { + try (transaction) { + applyDepthAndBlurInternal(transaction, applyImmediately, skipSimilarBlur); } } - protected void onInvalidSurface() { - } - - protected void applyDepthAndBlur() { + private void applyDepthAndBlurInternal(SurfaceControl.Transaction transaction, + boolean applyImmediately, boolean skipSimilarBlur) { float depth = mDepth; IBinder windowToken = mLauncher.getRootView().getWindowToken(); if (windowToken != null) { if (enableScalingRevealHomeAnimation()) { mWallpaperManager.setWallpaperZoomOut(windowToken, depth); } else { - // The API's full zoom-out is three times larger than the zoom-out we apply to - // the + // The API's full zoom-out is three times larger than the zoom-out we apply + // to the // icons. To keep the two consistent throughout the animation while keeping // Launcher's concept of full depth unchanged, we divide the depth by 3 here. mWallpaperManager.setWallpaperZoomOut(windowToken, depth / 3); @@ -151,11 +212,11 @@ public class BaseDepthController { if (!BlurUtils.supportsBlursOnWindows()) { return; } - if (mSurface == null) { + if (mBaseSurface == null) { Log.d(TAG, "mSurface is null and mCurrentBlur is: " + mCurrentBlur); return; } - if (!mSurface.isValid()) { + if (!mBaseSurface.isValid()) { Log.d(TAG, "mSurface is not valid"); mWaitingOnSurfaceValidity = true; onInvalidSurface(); @@ -171,37 +232,130 @@ public class BaseDepthController { } else { blurAmount = depth; } - mCurrentBlur = !mCrossWindowBlursEnabled || hasOpaqueBg || mPauseBlurs - ? 0 - : (int) (blurAmount * mMaxBlurRadius); + SurfaceControl blurSurface = + enableOverviewBackgroundWallpaperBlur() && mBlurSurface != null ? mBlurSurface + : mBaseSurface; - SurfaceControl.Transaction transaction = new SurfaceControl.Transaction() - .setBackgroundBlurRadius(mSurface, mCurrentBlur) - .setOpaque(mSurface, isSurfaceOpaque); + int previousBlur = mCurrentBlur; + int newBlur = mCrossWindowBlursEnabled && !hasOpaqueBg && !mPauseBlurs ? (int) (blurAmount + * mMaxBlurRadius) : 0; + int delta = Math.abs(newBlur - previousBlur); + if (skipSimilarBlur && delta < Utilities.dpToPx(1) && newBlur != 0 && previousBlur != 0 + && blurAmount != 1f) { + Log.d(TAG, "Skipping small blur delta. newBlur: " + newBlur + " previousBlur: " + + previousBlur + " delta: " + delta + " surface: " + blurSurface); + return; + } + mCurrentBlur = newBlur; + Log.v(TAG, "Applying blur: " + mCurrentBlur + " to " + blurSurface); - // Set early wake-up flags when we know we're executing an expensive operation, - // this way - // SurfaceFlinger will adjust its internal offsets to avoid jank. - boolean wantsEarlyWakeUp = depth > 0 && depth < 1; - if (wantsEarlyWakeUp && !mInEarlyWakeUp) { - transaction.setEarlyWakeupStart(); - mInEarlyWakeUp = true; - } else if (!wantsEarlyWakeUp && mInEarlyWakeUp) { - transaction.setEarlyWakeupEnd(); - mInEarlyWakeUp = false; + final SurfaceControl.Transaction finalTransaction = + transaction == null ? createTransaction() : transaction; + try (finalTransaction) { + finalTransaction.setBackgroundBlurRadius(blurSurface, mCurrentBlur) + .setOpaque(blurSurface, isSurfaceOpaque); + // Set early wake-up flags when we know we're executing an expensive operation, this way + // SurfaceFlinger will adjust its internal offsets to avoid jank. + boolean wantsEarlyWakeUp = blurAmount > 0 && blurAmount < 1; + if (wantsEarlyWakeUp && !mInEarlyWakeUp) { + try { + setEarlyWakeup(finalTransaction, true); + } catch (NoSuchMethodError e) { + // LC-Ignored: wtf? + } + } else if (!wantsEarlyWakeUp && mInEarlyWakeUp) { + try { + setEarlyWakeup(finalTransaction, false); + } catch (NoSuchMethodError e) { + // LC-Ignored: wtf? + } + } + + if (applyImmediately) { + finalTransaction.apply(); + } else { + AttachedSurfaceControl rootSurfaceControl = + mLauncher.getRootView().getRootSurfaceControl(); + if (rootSurfaceControl != null) { + rootSurfaceControl.applyTransactionOnDraw(finalTransaction); + } + } } - AttachedSurfaceControl rootSurfaceControl = mLauncher.getRootView().getRootSurfaceControl(); - if (rootSurfaceControl != null) { - rootSurfaceControl.applyTransactionOnDraw(transaction); + blurWorkspaceDepthTargets(); + } + + /** + * Sets the early wakeup state. + * + * @param inEarlyWakeUp whether SurfaceFlinger's early wakeup timing should be active. + */ + public void setEarlyWakeup(boolean inEarlyWakeUp) { + if (mInEarlyWakeUp == inEarlyWakeUp) { + return; } + try (SurfaceControl.Transaction transaction = createTransaction()) { + setEarlyWakeup(transaction, inEarlyWakeUp); + transaction.apply(); + } + } + + /** + * Sets the early wakeup state. + * + * @param transaction transaction to apply to. + * @param start whether to start or end the early wakeup. + */ + protected void setEarlyWakeup(@NonNull SurfaceControl.Transaction transaction, boolean start) { + if (mInEarlyWakeUp == start) { + return; + } + Log.d(TAG, "setEarlyWakeup: " + start); + if (start) { + Trace.instantForTrack(TRACE_TAG_APP, TAG, "notifyRendererForGpuLoadUp"); + mLauncher.getRootView().getViewRootImpl().notifyRendererForGpuLoadUp("applyBlur"); + transaction.setEarlyWakeupStart(mEarlyWakeupInfo); + } else { + transaction.setEarlyWakeupEnd(mEarlyWakeupInfo); + } + mInEarlyWakeUp = start; + } + + /** @return {@code true} if the workspace should be blurred. */ + @VisibleForTesting + public boolean blurWorkspaceDepthTargets() { + if (!Flags.allAppsBlur()) { + return false; + } + StateManager stateManager = mLauncher.getStateManager(); + LauncherState targetState = stateManager.getTargetState() != null + ? stateManager.getTargetState() : stateManager.getState(); + // Only blur workspace if the current state wants to blur based on the target state. + boolean shouldBlurWorkspace = + stateManager.getCurrentStableState().shouldBlurWorkspace(targetState); + + RenderEffect blurEffect = shouldBlurWorkspace && mCurrentBlur > 0 + ? RenderEffect.createBlurEffect(mCurrentBlur, mCurrentBlur, Shader.TileMode.DECAL) + // If blur is not desired, clear the blur effect from the depth targets. + : null; + mLauncher.getDepthBlurTargets().forEach(target -> target.setRenderEffect(blurEffect)); + return shouldBlurWorkspace; } private void setDepth(float depth) { depth = Utilities.boundToRange(depth, 0, 1); - // Round out the depth to dedupe frequent, non-perceptable updates - int depthI = (int) (depth * 256); - float depthF = depthI / 256f; + // Depth of the Launcher state we are in or transitioning to. + float targetStateDepth = mLauncher.getStateManager().getState().getDepth(mLauncher); + + float depthF; + if (depth == targetStateDepth) { + // Always apply the target state depth. + depthF = depth; + } else { + // Round out the depth to dedupe frequent, non-perceptable updates + int depthI = (int) (depth * 256); + depthF = depthI / 256f; + } if (Float.compare(mDepth, depthF) == 0) { return; } @@ -209,15 +363,65 @@ public class BaseDepthController { applyDepthAndBlur(); } + /** + * Sets the lowest surface that should not be blurred. + *

+ * Blur is applied to below {@link #mBaseSurfaceOverride}. When set to {@code null}, blur is + * applied to below {@link #mBaseSurface}. + *

+ */ + public void setBaseSurfaceOverride(@Nullable SurfaceControl baseSurfaceOverride, + boolean applyOnDraw) { + if (mBaseSurfaceOverride != baseSurfaceOverride) { + boolean applyImmediately = mBaseSurfaceOverride != null && baseSurfaceOverride == null + && !applyOnDraw; + mBaseSurfaceOverride = baseSurfaceOverride; + Log.d(TAG, "setBaseSurfaceOverride: applying blur behind leash " + baseSurfaceOverride); + SurfaceControl.Transaction transaction = setupBlurSurface(); + applyDepthAndBlur(transaction, applyImmediately, /* skipSimilarBlur */ false); + } + } + + private @Nullable SurfaceControl.Transaction setupBlurSurface() { + SurfaceControl.Transaction transaction = null; + if (mBaseSurface != null && mBaseSurfaceOverride != null) { + transaction = createTransaction().setBackgroundBlurRadius(mBaseSurface, 0) + .setOpaque(mBaseSurface, false); + if (mBlurSurface == null) { + mBlurSurface = new SurfaceControl.Builder() + .setName("Overview Blur") + .setHidden(false) + .build(); + Log.d(TAG, + "setupBlurSurface: creating Overview Blur surface " + mBlurSurface); + transaction.reparent(mBlurSurface, mBaseSurface); + Log.d(TAG, + "setupBlurSurface: reparenting " + mBlurSurface + " to " + mBaseSurface); + } + transaction.setRelativeLayer(mBlurSurface, mBaseSurfaceOverride, -1); + Log.d(TAG, "setupBlurSurface: relayering to leash " + mBaseSurfaceOverride); + } else if (mBlurSurface != null) { + Log.d(TAG, "setupBlurSurface: removing blur surface " + mBlurSurface); + transaction = createTransaction().remove(mBlurSurface); + mBlurSurface = null; + } + return transaction; + } + /** * Sets the specified app target surface to apply the blur to. */ - protected void setSurface(SurfaceControl surface) { - if (mSurface != surface || mWaitingOnSurfaceValidity) { - mSurface = surface; + protected void setBaseSurface(SurfaceControl baseSurface) { + if (mBaseSurface != baseSurface || mWaitingOnSurfaceValidity) { + mBaseSurface = baseSurface; Log.d(TAG, "setSurface:\n\tmWaitingOnSurfaceValidity: " + mWaitingOnSurfaceValidity - + "\n\tmSurface: " + mSurface); - applyDepthAndBlur(); + + "\n\tmBaseSurface: " + mBaseSurface); + SurfaceControl.Transaction transaction = null; + if (enableOverviewBackgroundWallpaperBlur()) { + transaction = setupBlurSurface(); + } + applyDepthAndBlur(transaction, /* applyImmediately */ false, + /* skipSimilarBlur */ false); } } @@ -226,6 +430,10 @@ public class BaseDepthController { * The blur percentage grows linearly with depth, and maxes out at 30% depth. */ private static float mapDepthToBlur(float depth) { - return Math.min(3 * depth, 1f); + return Interpolators.clampToProgress(depth, 0, 0.3f); + } + + private SurfaceControl.Transaction createTransaction() { + return new SurfaceControl.Transaction(); } } diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt index 7e51fcfedc..ac6e1ccc27 100644 --- a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt +++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt @@ -63,7 +63,7 @@ private constructor( const val DEFAULT_BORDER_COLOR = Color.WHITE private const val DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300L private const val DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133L - private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE + val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE /** * Creates a BorderAnimator that simply draws the border outside the bound of the target @@ -114,6 +114,8 @@ private constructor( * * @param borderRadiusPx the radius of the border's corners, in pixels * @param borderWidthPx the width of the border, in pixels + * @param borderStrokePx the stroke width used to paint the border, in pixels. If smaller + * than border width, it gets drawn at the outside edge of the border. * @param boundsBuilder callback to update the border bounds * @param targetView the view that will be drawing the border * @param contentView the view around which the border will be drawn. this view will be @@ -128,6 +130,7 @@ private constructor( fun createScalingBorderAnimator( @Px borderRadiusPx: Int, @Px borderWidthPx: Int, + @Px borderStrokePx: Int, boundsBuilder: (rect: Rect?) -> Unit, targetView: View, contentView: View, @@ -139,7 +142,13 @@ private constructor( return BorderAnimator( borderRadiusPx, borderColor, - ScalingParams(borderWidthPx, boundsBuilder, targetView, contentView), + ScalingParams( + borderWidthPx, + borderStrokePx, + boundsBuilder, + targetView, + contentView, + ), appearanceDurationMs, disappearanceDurationMs, interpolator, @@ -151,7 +160,7 @@ private constructor( val interpolatedProgress = interpolator.getInterpolation(borderAnimationProgress.value) borderAnimationParams.animationProgress = interpolatedProgress borderPaint.alpha = (255 * interpolatedProgress).roundToInt() - borderPaint.strokeWidth = borderAnimationParams.borderWidth + borderPaint.strokeWidth = borderAnimationParams.borderStroke borderAnimationParams.targetView.invalidate() } @@ -170,7 +179,7 @@ private constructor( /* bottom= */ borderBounds.bottom - alignmentAdjustment, /* rx= */ radius, /* ry= */ radius, - /* paint= */ borderPaint + /* paint= */ borderPaint, ) } } @@ -212,6 +221,7 @@ private constructor( /** Params for handling different target view layout situations. */ private abstract class BorderAnimationParams( @field:Px @param:Px val borderWidthPx: Int, + @field:Px @param:Px val borderStrokePx: Int, private val boundsBuilder: (rect: Rect) -> Unit, val targetView: View, ) { @@ -222,12 +232,12 @@ private constructor( abstract val alignmentAdjustmentInset: Int abstract val radiusAdjustment: Float - val borderWidth: Float - get() = borderWidthPx * animationProgress + val borderStroke: Float + get() = borderStrokePx * animationProgress val alignmentAdjustment: Float // Outset the border by half the width to create an outwards-growth animation - get() = -borderWidth / 2f + alignmentAdjustmentInset + get() = -borderStroke / 2f + alignmentAdjustmentInset open fun onShowBorder() { if (layoutChangeListener == null) { @@ -253,7 +263,7 @@ private constructor( @Px borderWidthPx: Int, boundsBuilder: (Rect) -> Unit, targetView: View, - ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) { + ) : BorderAnimationParams(borderWidthPx, borderWidthPx, boundsBuilder, targetView) { override val alignmentAdjustmentInset = 0 override val radiusAdjustment: Float get() = -alignmentAdjustment @@ -265,12 +275,13 @@ private constructor( */ private class ScalingParams( @Px borderWidthPx: Int, + @Px borderStrokePx: Int, boundsBuilder: (rect: Rect?) -> Unit, targetView: View, private val contentView: View, - ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) { + ) : BorderAnimationParams(borderWidthPx, borderStrokePx, boundsBuilder, targetView) { // Inset the border since we are scaling the container up - override val alignmentAdjustmentInset = borderWidthPx + override val alignmentAdjustmentInset = borderStrokePx override val radiusAdjustment: Float // Increase the radius since we are scaling the container up get() = alignmentAdjustment diff --git a/quickstep/src/com/android/quickstep/util/CachedEventDispatcher.java b/quickstep/src/com/android/quickstep/util/CachedEventDispatcher.java index 194c7d4f41..6b00a1d5c1 100644 --- a/quickstep/src/com/android/quickstep/util/CachedEventDispatcher.java +++ b/quickstep/src/com/android/quickstep/util/CachedEventDispatcher.java @@ -63,6 +63,11 @@ public class CachedEventDispatcher { mLastEvent = null; } + /** Clear the consumer. */ + public void clearConsumer() { + mConsumer = null; + } + public boolean hasConsumer() { return mConsumer != null; } diff --git a/quickstep/src/com/android/quickstep/util/ChoreographerFrameRateTracker.kt b/quickstep/src/com/android/quickstep/util/ChoreographerFrameRateTracker.kt new file mode 100644 index 0000000000..1b61a6af6b --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ChoreographerFrameRateTracker.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.util.TimeUtils +import android.view.Choreographer +import com.android.launcher3.util.window.RefreshRateTracker + +/** [RefreshRateTracker] using main thread [Choreographer] */ +object ChoreographerFrameRateTracker : RefreshRateTracker { + + override val singleFrameMs: Int + get() = + Choreographer.getMainThreadInstance()?.let { + (it.frameIntervalNanos / TimeUtils.NANOS_PER_MS).toInt().coerceAtLeast(1) + } ?: 1 +} diff --git a/quickstep/src/com/android/quickstep/util/ClickListeners.kt b/quickstep/src/com/android/quickstep/util/ClickListeners.kt new file mode 100644 index 0000000000..e827e9fe9b --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ClickListeners.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.app.ActivityOptions +import android.content.ActivityNotFoundException +import android.content.Intent +import android.util.Log +import android.view.View +import com.android.quickstep.task.thumbnail.TaskContentView + +/** + * Sets a click listener on the view that launches an activity using the provided [intent] + * + * Performs scale up animation. + * + * @property intent the intent to use for launching the activity + * @property targetDescription a short text describing the target activity beings opened that can be + * used for logging (e.g. usage settings for task A). + */ +fun View.setActivityStarterClickListener(intent: Intent, targetDescription: String) { + setOnClickListener { view -> + try { + val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height) + context.startActivity(intent, options.toBundle()) + } catch (e: ActivityNotFoundException) { + Log.e(TaskContentView.TAG, "Failed to open $targetDescription ", e) + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ContextInitListener.java similarity index 63% rename from quickstep/src/com/android/quickstep/util/ActivityInitListener.java rename to quickstep/src/com/android/quickstep/util/ContextInitListener.java index 5efbb4074a..49f1463c9d 100644 --- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java +++ b/quickstep/src/com/android/quickstep/util/ContextInitListener.java @@ -15,17 +15,17 @@ */ package com.android.quickstep.util; -import com.android.launcher3.BaseActivity; -import com.android.launcher3.util.ActivityTracker; -import com.android.launcher3.util.ActivityTracker.SchedulerCallback; +import com.android.launcher3.util.ContextTracker; +import com.android.launcher3.util.ContextTracker.SchedulerCallback; +import com.android.launcher3.views.ActivityContext; import java.util.function.BiPredicate; -public class ActivityInitListener implements - SchedulerCallback { +public class ContextInitListener implements + SchedulerCallback { - private BiPredicate mOnInitListener; - private final ActivityTracker mActivityTracker; + private BiPredicate mOnInitListener; + private final ContextTracker mContextTracker; private boolean mIsRegistered = false; @@ -34,23 +34,23 @@ public class ActivityInitListener implements * return true to continue receiving callbacks (ie. for if the activity is * recreated). */ - public ActivityInitListener(BiPredicate onInitListener, - ActivityTracker tracker) { + public ContextInitListener(BiPredicate onInitListener, + ContextTracker tracker) { mOnInitListener = onInitListener; - mActivityTracker = tracker; + mContextTracker = tracker; } @Override - public final boolean init(T activity, boolean alreadyOnHome) { + public final boolean init(CONTEXT activity, boolean isHomeStarted) { if (!mIsRegistered) { // Don't receive any more updates return false; } - return handleInit(activity, alreadyOnHome); + return handleInit(activity, isHomeStarted); } - protected boolean handleInit(T activity, boolean alreadyOnHome) { - return mOnInitListener.test(activity, alreadyOnHome); + protected boolean handleInit(CONTEXT activity, boolean isHomeStarted) { + return mOnInitListener.test(activity, isHomeStarted); } /** @@ -59,14 +59,14 @@ public class ActivityInitListener implements */ public void register(String reasonString) { mIsRegistered = true; - mActivityTracker.registerCallback(this, reasonString); + mContextTracker.registerCallback(this, reasonString); } /** * After calling this, we won't {@link #init} even when the activity is ready. */ public void unregister(String reasonString) { - mActivityTracker.unregisterCallback(this, reasonString); + mContextTracker.unregisterCallback(this, reasonString); mIsRegistered = false; mOnInitListener = null; } diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt new file mode 100644 index 0000000000..7ec605d2a3 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt @@ -0,0 +1,112 @@ +/* + * 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.quickstep.util + +import android.content.Context +import android.os.VibrationEffect +import android.os.VibrationEffect.Composition +import android.os.Vibrator +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.DaggerSingletonObject +import com.android.launcher3.util.VibratorWrapper +import com.android.quickstep.DeviceConfigWrapper.Companion.get +import com.android.quickstep.dagger.QuickstepBaseAppComponent +import javax.inject.Inject +import kotlin.math.pow + +/** Manages haptics relating to Contextual Search invocations. */ +@LauncherAppSingleton +class ContextualSearchHapticManager +@Inject +internal constructor( + @ApplicationContext private val context: Context, + private val contextualSearchStateManager: ContextualSearchStateManager, + private val vibratorWrapper: VibratorWrapper, +) { + + private var searchEffect = createSearchEffect() + + private fun createSearchEffect() = + if ( + context + .getSystemService(Vibrator::class.java)!! + .areAllPrimitivesSupported(Composition.PRIMITIVE_TICK) + ) { + VibrationEffect.startComposition() + .addPrimitive(Composition.PRIMITIVE_TICK, 1f) + .compose() + } else { + // fallback for devices without composition support + VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK) + } + + /** Indicates that search has been invoked. */ + fun vibrateForSearch() { + searchEffect.let { vibratorWrapper.vibrate(it) } + } + + /** Indicates that search will be invoked if the current gesture is maintained. */ + fun vibrateForSearchHint() { + val navbarConfig = get() + // Whether we should play the hint (ramp up) haptic + val shouldVibrate: Boolean = + if ( + context + .getSystemService(Vibrator::class.java)!! + .areAllPrimitivesSupported(Composition.PRIMITIVE_LOW_TICK) + ) { + if (contextualSearchStateManager.shouldPlayHapticOverride.isPresent) { + contextualSearchStateManager.shouldPlayHapticOverride.get() + } else { + navbarConfig.enableSearchHapticHint + } + } else { + false + } + + if (shouldVibrate) { + val startScale = navbarConfig.lpnhHapticHintStartScalePercent / 100f + val endScale = navbarConfig.lpnhHapticHintEndScalePercent / 100f + val scaleExponent = navbarConfig.lpnhHapticHintScaleExponent + val iterations = navbarConfig.lpnhHapticHintIterations + val delayMs = navbarConfig.lpnhHapticHintDelay + val composition = VibrationEffect.startComposition() + for (i in 0 until iterations) { + val t = i / (iterations - 1f) + val scale = + ((1 - t) * startScale + t * endScale) + .toDouble() + .pow(scaleExponent.toDouble()) + .toFloat() + if (i == 0) { + // Adds a delay before the ramp starts + composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale, delayMs) + } else { + composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale) + } + } + vibratorWrapper.vibrate(composition.compose()) + } + } + + companion object { + @JvmField + val INSTANCE = + DaggerSingletonObject(QuickstepBaseAppComponent::getContextualSearchHapticManager) + } +} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt new file mode 100644 index 0000000000..aabea65917 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt @@ -0,0 +1,274 @@ +/* + * 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.quickstep.util + +import android.app.contextualsearch.ContextualSearchManager +import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_HOME +import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH +import android.content.Context +import android.util.Log +import android.view.Display.DEFAULT_DISPLAY +import androidx.annotation.VisibleForTesting +import com.android.internal.app.AssistUtils +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME +import com.android.quickstep.BaseContainerInterface +import com.android.quickstep.DeviceConfigWrapper +import com.android.quickstep.OverviewComponentObserver +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.TopTaskTracker +import com.android.quickstep.views.RecentsView +import com.android.systemui.shared.system.QuickStepContract +import javax.inject.Inject + +/** Handles invocations and checks for Contextual Search. */ +class ContextualSearchInvoker +internal constructor( + private val context: Context, + private val contextualSearchStateManager: ContextualSearchStateManager, + private val topTaskTracker: TopTaskTracker, + private val systemUiProxy: SystemUiProxy, + private val statsLogManager: StatsLogManager, + private val contextualSearchHapticManager: ContextualSearchHapticManager, + private val contextualSearchManager: ContextualSearchManager?, +) { + constructor( + context: Context + ) : this( + context, + ContextualSearchStateManager.INSTANCE[context], + TopTaskTracker.INSTANCE[context], + SystemUiProxy.INSTANCE[context], + StatsLogManager.newInstance(context), + ContextualSearchHapticManager.INSTANCE[context], + context.getSystemService(ContextualSearchManager::class.java), + ) + + @Inject + constructor( + @ApplicationContext context: Context, + contextualSearchStateManager: ContextualSearchStateManager, + topTaskTracker: TopTaskTracker, + systemUiProxy: SystemUiProxy, + logManagerFactory: StatsLogManager.StatsLogManagerFactory, + hapticManager: ContextualSearchHapticManager, + ) : this( + context, + contextualSearchStateManager, + topTaskTracker, + systemUiProxy, + logManagerFactory.create(context), + hapticManager, + context.getSystemService(ContextualSearchManager::class.java), + ) + + /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ + fun getSysUiAssistOverrideInvocationTypes(): IntArray { + val overrideInvocationTypes = com.android.launcher3.util.IntArray() + if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) + } + return overrideInvocationTypes.toArray() + } + + /** + * @return `true` if the override was handled, i.e. an assist surface was shown or the request + * should be ignored. `false` means the caller should start assist another way. + */ + fun tryStartAssistOverride(invocationType: Int): Boolean { + if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) { + if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + // When Contextual Search is disabled, fall back to Assistant. + return false + } + + val success = show(ENTRYPOINT_LONG_PRESS_HOME) + if (success) { + val runningPackage = + TopTaskTracker.INSTANCE[context].getCachedTopTask( + /* filterOnlyVisibleRecents */ true, + DEFAULT_DISPLAY, + ) + .getPackageName() + statsLogManager + .logger() + .withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME) + } + + // Regardless of success, do not fall back to other assistant. + return true + } + return false + } + + /** + * Invoke Contextual Search via ContextualSearchService if availability checks are successful + * + * @param entryPoint one of the ENTRY_POINT_* constants defined in this class + * @return true if invocation was successful, false otherwise + */ + fun show(entryPoint: Int): Boolean { + return if (!runContextualSearchInvocationChecksAndLogFailures()) false + else invokeContextualSearchUnchecked(entryPoint) + } + + /** + * Run availability checks and log errors to WW. If successful the caller is expected to call + * {@link invokeContextualSearchUnchecked} + * + * @return true if availability checks were successful, false otherwise. + */ + fun runContextualSearchInvocationChecksAndLogFailures(): Boolean { + if ( + contextualSearchManager == null || + !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH) + ) { + Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager") + statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR) + return false + } + if (!contextualSearchStateManager.isContextualSearchSettingEnabled) { + Log.i(TAG, "Contextual Search invocation failed: setting disabled") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED) + return false + } + if (isNotificationShadeShowing()) { + Log.i(TAG, "Contextual Search invocation failed: notification shade") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE) + return false + } + if (isKeyguardShowing()) { + Log.i(TAG, "Contextual Search invocation attempted: keyguard") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD) + if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) { + Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed") + return false + } else if (!contextualSearchStateManager.supportsShowWhenLocked()) { + Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard") + return false + } + } + if (isInSplitscreen()) { + Log.i(TAG, "Contextual Search invocation attempted: splitscreen") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN) + if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) { + Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed") + return false + } + } + if (!contextualSearchStateManager.isContextualSearchIntentAvailable) { + Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter") + statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE) + return false + } + + return true + } + + /** + * Invoke Contextual Search via ContextualSearchService and do haptic + * + * @param entryPoint Entry point identifier, passed to ContextualSearchService. + * @return true if invocation was successful, false otherwise + */ + fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean { + return invokeContextualSearchUnchecked(entryPoint, withHaptic = true) + } + + private fun invokeContextualSearchUnchecked( + entryPoint: Int, + withHaptic: Boolean = false, + ): Boolean { + if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) { + contextualSearchHapticManager.vibrateForSearch() + } + if (contextualSearchManager == null) { + return false + } + val recentsContainerInterface = getRecentsContainerInterface() + if (recentsContainerInterface?.isInLiveTileMode() == true) { + Log.i(TAG, "Contextual Search invocation attempted: live tile") + endLiveTileMode(recentsContainerInterface) { + contextualSearchManager.startContextualSearch(entryPoint) + } + } else { + contextualSearchManager.startContextualSearch(entryPoint) + } + return true + } + + private fun isInSplitscreen(): Boolean { + return topTaskTracker.getRunningSplitTaskIds().isNotEmpty() + } + + private fun isNotificationShadeShowing(): Boolean { + return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L + } + + private fun isKeyguardShowing(): Boolean { + return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L + } + + @VisibleForTesting + fun getRecentsContainerInterface(): BaseContainerInterface<*, *>? { + return OverviewComponentObserver.INSTANCE.get(context) + .getContainerInterface(DEFAULT_DISPLAY) + } + + /** + * End the live tile mode. + * + * @param onCompleteRunnable Runnable to run when the live tile is paused. May run immediately. + */ + private fun endLiveTileMode( + recentsContainerInterface: BaseContainerInterface<*, *>?, + onCompleteRunnable: Runnable, + ) { + val recentsViewContainer = recentsContainerInterface?.createdContainer + if (recentsViewContainer == null) { + onCompleteRunnable.run() + return + } + val recentsView: RecentsView<*, *> = recentsViewContainer.getOverviewPanel() + recentsView.switchToScreenshot { + recentsView.finishRecentsAnimation( + true, /* toRecents */ + false, /* shouldPip */ + onCompleteRunnable, + ) + } + } + + companion object { + private const val TAG = "ContextualSearchInvoker" + const val SHADE_EXPANDED_SYSUI_FLAGS = + QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or + QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED + const val KEYGUARD_SHOWING_SYSUI_FLAGS = + (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or + QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or + QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) + } +} diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java new file mode 100644 index 0000000000..136c496dce --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static android.app.contextualsearch.ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH; +import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_SYSTEM_ACTION; +import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN; + +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.content.IIntentReceiver; +import android.content.IIntentSender; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.CallSuper; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.R; +import com.android.launcher3.dagger.ApplicationContext; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.util.DaggerSingletonObject; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.EventLogArray; +import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.util.SimpleBroadcastReceiver; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TopTaskTracker; + +import java.io.PrintWriter; +import java.util.Optional; + +import javax.inject.Inject; + +/** Long-lived class to manage Contextual Search states like the user setting and availability. */ +@LauncherAppSingleton +public class ContextualSearchStateManager { + + public static final DaggerSingletonObject INSTANCE = + new DaggerSingletonObject<>(LauncherAppComponent::getContextualSearchStateManager); + + private static final String TAG = "ContextualSearchStMgr"; + private static final int MAX_DEBUG_EVENT_SIZE = 20; + private static final Uri SEARCH_ALL_ENTRYPOINTS_ENABLED_URI = + Settings.Secure.getUriFor(Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED); + + private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi; + private final SimpleBroadcastReceiver mContextualSearchPackageReceiver; + protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE); + + // Cached value whether the ContextualSearch intent filter matched any enabled components. + private boolean mIsContextualSearchIntentAvailable; + private boolean mIsContextualSearchSettingEnabled; + + protected final Context mContext; + protected final String mContextualSearchPackage; + protected final SystemUiProxy mSystemUiProxy; + protected final TopTaskTracker mTopTaskTracker; + + @Inject + public ContextualSearchStateManager( + @ApplicationContext Context context, + SettingsCache settingsCache, + SystemUiProxy systemUiProxy, + TopTaskTracker topTaskTracker, + DaggerSingletonTracker lifeCycle) { + mContext = context; + mContextualSearchPackageReceiver = + new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, + (unused) -> requestUpdateProperties()); + mContextualSearchPackage = mContext.getResources().getString( + com.android.internal.R.string.config_defaultContextualSearchPackageName); + mSystemUiProxy = systemUiProxy; + mTopTaskTracker = topTaskTracker; + + if (areAllContextualSearchFlagsDisabled() + || !context.getPackageManager().hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) { + // If we had previously registered a SystemAction which is no longer valid, we need to + // unregister it here. + unregisterSearchScreenSystemAction(); + // Don't listen for stuff we aren't gonna use. + return; + } + + requestUpdateProperties(); + registerSearchScreenSystemAction(); + mContextualSearchPackageReceiver.registerPkgActions( + mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED); + + SettingsCache.OnChangeListener settingChangedListener = + isEnabled -> mIsContextualSearchSettingEnabled = isEnabled; + settingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, settingChangedListener); + mIsContextualSearchSettingEnabled = + settingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI); + + systemUiProxy.addOnStateChangeListener(mSysUiStateChangeListener); + + lifeCycle.addCloseable(() -> { + mContextualSearchPackageReceiver.unregisterReceiverSafely(); + unregisterSearchScreenSystemAction(); + settingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, settingChangedListener); + systemUiProxy.removeOnStateChangeListener(mSysUiStateChangeListener); + }); + } + + /** Return {@code true} if the Settings toggle is enabled. */ + public final boolean isContextualSearchSettingEnabled() { + return mIsContextualSearchSettingEnabled; + } + + /** Whether search supports showing on the lockscreen. */ + protected boolean supportsShowWhenLocked() { + return false; + } + + /** Whether ContextualSearchService invocation path is available. */ + @VisibleForTesting + protected final boolean isContextualSearchIntentAvailable() { + return mIsContextualSearchIntentAvailable; + } + + /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */ + public Optional getLPNHDurationMillis() { + return Optional.empty(); + } + + /** + * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant. + */ + public Optional getLPNHCustomSlopMultiplier() { + return Optional.empty(); + } + + /** Get the Launcher overridden long press home duration to trigger Assistant. */ + public Optional getLPHDurationMillis() { + return Optional.empty(); + } + + /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */ + public Optional getLPHCustomSlopMultiplier() { + return Optional.empty(); + } + + /** Get the long press duration data source. */ + public int getDurationDataSource() { + return 0; + } + + /** Get the long press touch slop multiplier data source. */ + public int getSlopDataSource() { + return 0; + } + + /** + * Get the User group based on the behavior to trigger Assistant. + */ + public Optional getLPUserGroup() { + return Optional.empty(); + } + + /** Get the haptic bit overridden by AGSA. */ + public Optional getShouldPlayHapticOverride() { + return Optional.empty(); + } + + protected boolean isInvocationAllowedOnKeyguard() { + return false; + } + + protected boolean isInvocationAllowedInSplitscreen() { + return true; + } + + @CallSuper + protected boolean areAllContextualSearchFlagsDisabled() { + return !DeviceConfigWrapper.get().getEnableLongPressNavHandle(); + } + + @CallSuper + protected void requestUpdateProperties() { + UI_HELPER_EXECUTOR.execute(() -> { + // Check that Contextual Search intent filters are enabled. + Intent csIntent = new Intent(ACTION_LAUNCH_CONTEXTUAL_SEARCH).setPackage( + mContextualSearchPackage); + mIsContextualSearchIntentAvailable = + !mContext.getPackageManager().queryIntentActivities(csIntent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE).isEmpty(); + + addEventLog("Updated isContextualSearchIntentAvailable", + mIsContextualSearchIntentAvailable); + }); + } + + protected final void updateOverridesToSysUi() { + // LPH commit haptic is always enabled + mSystemUiProxy.setOverrideHomeButtonLongPress( + getLPHDurationMillis().orElse(0L), getLPHCustomSlopMultiplier().orElse(0f), true); + Log.i(TAG, "Sent LPH override to sysui: " + getLPHDurationMillis().orElse(0L) + ";" + + getLPHCustomSlopMultiplier().orElse(0f)); + } + + private void registerSearchScreenSystemAction() { + PendingIntent searchScreenPendingIntent = new PendingIntent(new IIntentSender.Stub() { + @Override + public void send(int i, Intent intent, String s, IBinder iBinder, + IIntentReceiver iIntentReceiver, String s1, Bundle bundle) + throws RemoteException { + // Delayed slightly to minimize chance of capturing the System Actions dialog. + UI_HELPER_EXECUTOR.getHandler().postDelayed( + () -> { + boolean contextualSearchInvoked = + new ContextualSearchInvoker(mContext).show( + ENTRYPOINT_SYSTEM_ACTION); + if (contextualSearchInvoked) { + String runningPackage = mTopTaskTracker.getCachedTopTask( + /* filterOnlyVisibleRecents */ true, + DEFAULT_DISPLAY).getPackageName(); + StatsLogManager.newInstance(mContext).logger() + .withPackageName(runningPackage) + .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION); + } + }, 200); + } + }); + + mContext.getSystemService(AccessibilityManager.class).registerSystemAction(new RemoteAction( + Icon.createWithResource(mContext, R.drawable.ic_allapps_search), + mContext.getString(R.string.search_gesture_feature_title), + mContext.getString(R.string.search_gesture_feature_title), + searchScreenPendingIntent), + SYSTEM_ACTION_ID_SEARCH_SCREEN); + } + + private void unregisterSearchScreenSystemAction() { + mContext.getSystemService(AccessibilityManager.class).unregisterSystemAction( + SYSTEM_ACTION_ID_SEARCH_SCREEN); + } + + /** Dump states. */ + public final void dump(String prefix, PrintWriter writer) { + synchronized (mEventLogArray) { + mEventLogArray.dump(prefix, writer); + } + } + + protected final void addEventLog(String event) { + synchronized (mEventLogArray) { + mEventLogArray.addLog(event); + } + } + + protected final void addEventLog(String event, boolean extras) { + synchronized (mEventLogArray) { + mEventLogArray.addLog(event, extras); + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/DesksUtils.kt b/quickstep/src/com/android/quickstep/util/DesksUtils.kt new file mode 100644 index 0000000000..521ba27a8e --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/DesksUtils.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.app.TaskInfo +import android.content.ComponentName +import android.content.res.Resources +import android.window.DesktopExperienceFlags +import com.android.systemui.shared.recents.model.Task + +class DesksUtils { + companion object { + val sysUiPackage = + Resources.getSystem().getString(com.android.internal.R.string.config_systemUi) + + @JvmStatic + fun areMultiDesksFlagsEnabled() = + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_FRONTEND.isTrue + + /** Returns true if this [task] contains the [DesktopWallpaperActivity]. */ + @JvmStatic + fun isDesktopWallpaperTask(task: Task) = + task.key.component?.let(::isDesktopWallpaperComponent) == true + + @JvmStatic + fun isDesktopWallpaperTask(taskInfo: TaskInfo): Boolean { + // TODO: b/403118101 - In some launcher tests, there is a task with baseIntent set to + // null. Remove this check after finding out how that task is created. + if (taskInfo.baseIntent == null) return false + return taskInfo.baseIntent.component?.let(::isDesktopWallpaperComponent) == true + } + + @JvmStatic + fun isDesktopWallpaperComponent(component: ComponentName) = + component.className.contains("DesktopWallpaperActivity") && + component.packageName.contains(sysUiPackage) + } +} diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java deleted file mode 100644 index 8d99069c19..0000000000 --- a/quickstep/src/com/android/quickstep/util/DesktopTask.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep.util; - -import androidx.annotation.NonNull; - -import com.android.quickstep.views.TaskView; -import com.android.systemui.shared.recents.model.Task; - -import java.util.List; - -/** - * A {@link Task} container that can contain N number of tasks that are part of the desktop in - * recent tasks list. - */ -public class DesktopTask extends GroupTask { - - @NonNull - public final List tasks; - - public DesktopTask(@NonNull List tasks) { - super(tasks.get(0), null, null, TaskView.Type.DESKTOP); - this.tasks = tasks; - } - - @Override - public boolean containsTask(int taskId) { - for (Task task : tasks) { - if (task.key.id == taskId) { - return true; - } - } - return false; - } - - @Override - public boolean hasMultipleTasks() { - return true; - } - - @Override - @NonNull - public List getTasks() { - return tasks; - } - - @Override - public DesktopTask copy() { - return new DesktopTask(tasks); - } - - @Override - public String toString() { - return "type=" + taskViewType + " tasks=" + tasks; - } - -} diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.kt b/quickstep/src/com/android/quickstep/util/DesktopTask.kt new file mode 100644 index 0000000000..e29c869c16 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/DesktopTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util + +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.Task +import java.util.Objects + +/** + * A [Task] container that can contain N number of tasks that are part of the desktop in recent + * tasks list. Note that desktops can be empty with no tasks in them. The [deskId], [displayId] + * makes sense only when the multiple desks feature is enabled. + */ +class DesktopTask(val deskId: Int, desktopDisplayId: Int, tasks: List) : + GroupTask(tasks, desktopDisplayId, TaskViewType.DESKTOP) { + + override fun copy() = DesktopTask(deskId, displayId, tasks) + + override fun toString() = "type=$taskViewType deskId=$deskId displayId=$displayId tasks=$tasks" + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o !is DesktopTask) return false + if (deskId != o.deskId) return false + if (displayId != o.displayId) return false + return super.equals(o) + } + + override fun hashCode() = Objects.hash(super.hashCode(), deskId, displayId) +} diff --git a/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt new file mode 100644 index 0000000000..e60e9f3959 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.app.TaskInfo +import android.view.Display.DEFAULT_DISPLAY +import android.view.Display.INVALID_DISPLAY +import com.android.systemui.shared.recents.model.Task + +/** Whether this displayId belongs to an external display */ +val Int.isExternalDisplay + get() = !isDefaultDisplay + +/** Whether this displayId belongs to the default display */ +val Int.isDefaultDisplay + get() = this == DEFAULT_DISPLAY + +val Int?.safeDisplayId + get() = + this.let { displayId -> + when (displayId) { + null -> DEFAULT_DISPLAY + INVALID_DISPLAY -> DEFAULT_DISPLAY + else -> displayId + } + } + +/** Returns displayId of this [Task], default to [DEFAULT_DISPLAY] */ +val Task?.safeDisplayId + get() = this?.key?.displayId.safeDisplayId + +/** Returns if this task belongs tto [DEFAULT_DISPLAY] */ +val Task?.isExternalDisplay + get() = safeDisplayId.isExternalDisplay + +/** Returns displayId of this [TaskInfo], default to [DEFAULT_DISPLAY] */ +val TaskInfo?.safeDisplayId + get() = this?.displayId.safeDisplayId diff --git a/quickstep/src/com/android/quickstep/util/FontUtils.kt b/quickstep/src/com/android/quickstep/util/FontUtils.kt new file mode 100644 index 0000000000..35a0fc7e1b --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/FontUtils.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Typeface +import com.android.wm.shell.shared.TypefaceUtils + +object FontUtils { + + private val baseTypeface = + Typeface.create(TypefaceUtils.FontFamily.GSF_LABEL_LARGE.value, Typeface.NORMAL) + + @JvmStatic + fun getTypeFace(resources: Resources): Typeface = + Typeface.create(baseTypeface, getFontWeight(resources), /* italic= */ false) + + fun getFontWeight(resources: Resources): Int { + val fontWeightAdjustment: Int = resources.configuration.fontWeightAdjustment + return if (fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) { + Typeface.Builder.NORMAL_WEIGHT + fontWeightAdjustment + } else { + Typeface.Builder.NORMAL_WEIGHT + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java deleted file mode 100644 index 945ffe31f6..0000000000 --- a/quickstep/src/com/android/quickstep/util/GroupTask.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep.util; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; -import com.android.quickstep.views.TaskView; -import com.android.systemui.shared.recents.model.Task; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** - * A {@link Task} container that can contain one or two tasks, depending on if the two tasks - * are represented as an app-pair in the recents task list. - */ -public class GroupTask { - @NonNull - public final Task task1; - @Nullable - public final Task task2; - @Nullable - public final SplitBounds mSplitBounds; - @TaskView.Type - public final int taskViewType; - - public GroupTask(@NonNull Task task) { - this(task, null, null); - } - - public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) { - this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE); - } - - protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds, - @TaskView.Type int taskViewType) { - task1 = t1; - task2 = t2; - mSplitBounds = splitBounds; - this.taskViewType = taskViewType; - } - - public boolean containsTask(int taskId) { - return task1.key.id == taskId || (task2 != null && task2.key.id == taskId); - } - - public boolean hasMultipleTasks() { - return task2 != null; - } - - /** - * Returns a List of all the Tasks in this GroupTask - */ - public List getTasks() { - if (task2 == null) { - return Collections.singletonList(task1); - } else { - return Arrays.asList(task1, task2); - } - } - - /** - * Create a copy of this instance - */ - public GroupTask copy() { - return new GroupTask( - new Task(task1), - task2 != null ? new Task(task2) : null, - mSplitBounds); - } - - @Override - public String toString() { - return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2; - } - -} diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.kt b/quickstep/src/com/android/quickstep/util/GroupTask.kt new file mode 100644 index 0000000000..9ba20673a1 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/GroupTask.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util + +import com.android.launcher3.model.data.TaskItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.split.SplitBounds +import java.util.Objects + +/** + * An abstract class for creating [Task] containers that can be [SingleTask]s, [SplitTask]s, or + * [DesktopTask]s in the recent tasks list. + */ +abstract class GroupTask( + val tasks: List, + val displayId: Int, + @JvmField val taskViewType: TaskViewType, +) { + + fun containsTask(taskId: Int) = tasks.any { it.key.id == taskId } + + /** + * Returns true if a task in this group has a package name that matches the given `packageName`. + */ + fun containsPackage(packageName: String?) = tasks.any { it.key.packageName == packageName } + + /** Returns true if a task in this group has the given displayId. */ + fun matchesDisplayId(displayId: Int) = displayId == this.displayId.safeDisplayId + + /** + * Returns true if a task in this group has a package name that matches the given `packageName`, + * and its user ID matches the given `userId`. + */ + fun containsPackage(packageName: String?, userId: Int) = + tasks.any { it.key.packageName == packageName && it.key.userId == userId } + + fun isEmpty() = tasks.isEmpty() + + /** Creates a copy of this instance */ + abstract fun copy(): GroupTask + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o !is GroupTask) return false + return taskViewType == o.taskViewType && tasks == o.tasks + } + + override fun hashCode() = Objects.hash(tasks, taskViewType) +} + +/** A [Task] container that must contain exactly one task in the recent tasks list. */ +class SingleTask(task: Task) : GroupTask(listOf(task), task.key.displayId, TaskViewType.SINGLE) { + + val task: Task + get() = tasks[0] + + override fun copy() = SingleTask(task) + + override fun toString() = "type=$taskViewType task=$task" + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o !is SingleTask) return false + return super.equals(o) + } + + companion object { + /** Creates a [TaskItemInfo] using the information of the SingleTask */ + fun createTaskItemInfo(task: SingleTask, wif: WorkspaceItemInfo): TaskItemInfo { + // TODO: b/344657629 - Support GroupTask in addition to SingleTask. + return TaskItemInfo(task.task.key.id, wif) + } + } + + override fun hashCode() = super.hashCode() +} + +/** + * A [Task] container that must contain exactly two tasks and split bounds to represent an app-pair + * in the recent tasks list. + */ +class SplitTask(task1: Task, task2: Task, val splitBounds: SplitBounds?) : + GroupTask(listOf(task1, task2), task1.key.displayId, TaskViewType.GROUPED) { + + val topLeftTask: Task + get() = + when { + splitBounds == null -> tasks[0] + splitBounds.leftTopTaskId == tasks[0].key.id -> tasks[0] + else -> tasks[1] + } + + val bottomRightTask: Task + get() = if (topLeftTask == tasks[0]) tasks[1] else tasks[0] + + override fun copy() = SplitTask(tasks[0], tasks[1], splitBounds) + + override fun toString() = + "type=$taskViewType topLeftTask=$topLeftTask bottomRightTask=$bottomRightTask" + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o !is SplitTask) return false + if (splitBounds != o.splitBounds) return false + return super.equals(o) + } + + override fun hashCode() = Objects.hash(super.hashCode(), splitBounds) +} diff --git a/quickstep/src/com/android/quickstep/util/IconLabelUtil.kt b/quickstep/src/com/android/quickstep/util/IconLabelUtil.kt new file mode 100644 index 0000000000..a876bca982 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/IconLabelUtil.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.ActivityInfo +import android.os.UserHandle +import com.android.launcher3.Utilities + +object IconLabelUtil { + @JvmStatic + @JvmOverloads + fun getBadgedContentDescription( + context: Context, + info: ActivityInfo, + userId: Int, + taskDescription: ActivityManager.TaskDescription? = null, + ): String { + val packageManager = context.packageManager + var taskLabel = taskDescription?.let { Utilities.trim(it.label) } + if (taskLabel.isNullOrEmpty()) { + taskLabel = Utilities.trim(info.loadLabel(packageManager)) + } + + val applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(packageManager)) + val badgedApplicationLabel = + if (userId != UserHandle.myUserId()) + packageManager + .getUserBadgedLabel(applicationLabel, UserHandle.of(userId)) + .toString() + else applicationLabel + return if (applicationLabel == taskLabel) badgedApplicationLabel + else "$badgedApplicationLabel $taskLabel" + } +} diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java index f659e16373..285c7624ae 100644 --- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java +++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java @@ -54,7 +54,6 @@ import com.android.internal.util.ScreenshotRequest; import com.android.launcher3.BuildConfig; import com.android.quickstep.SystemUiProxy; import com.android.systemui.shared.recents.model.Task; -import com.topjohnwu.superuser.Shell; import java.io.File; import java.io.FileOutputStream; @@ -62,6 +61,7 @@ import java.io.IOException; import java.util.function.BiFunction; import java.util.function.Supplier; +import com.topjohnwu.superuser.Shell; import app.lawnchair.compatlib.utils.BitmapUtil; /** @@ -83,17 +83,19 @@ public class ImageActionUtils { Rect screenshotBounds, Insets visibleInsets, Task.TaskKey task) { try { ScreenshotRequest request = - new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW) - .setTopComponent(task.sourceComponent) - .setTaskId(task.id) - .setUserId(task.userId) - .setBitmap(screenshot) - .setBoundsOnScreen(screenshotBounds) - .setInsets(visibleInsets) - .build(); + new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW) + .setTopComponent(task.sourceComponent) + .setTaskId(task.id) + .setUserId(task.userId) + .setBitmap(screenshot) + .setBoundsOnScreen(screenshotBounds) + .setInsets(visibleInsets) + .setDisplayId(task.displayId) + .build(); systemUiProxy.takeScreenshot(request); } catch (Throwable t) { try { + // Lawnchair-TODO-Merge: LC disabled this, but no code is in 16r2 // systemUiProxy.handleImageBundleAsScreenshot(BitmapUtil.hardwareBitmapToBundle(screenshot), // screenshotBounds, visibleInsets, task); } catch (Throwable ee) { diff --git a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java index cb44a1a0d8..fcf9ab1ad4 100644 --- a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java +++ b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java @@ -108,19 +108,28 @@ public class InputConsumerProxy { return false; } + final SimpleOrientationTouchTransformer touchTransformer = + SimpleOrientationTouchTransformer.INSTANCE.get(mContext); + final int viewRotation = mRotationSupplier.get(); + final boolean needTransform = viewRotation != ev.getSurfaceRotation(); if (action == ACTION_DOWN) { mTouchInProgress = true; + if (needTransform) { + touchTransformer.updateTouchingOrientation(viewRotation); + } initInputConsumerIfNeeded(/* isFromTouchDown= */ true); } else if (action == ACTION_CANCEL || action == ACTION_UP) { // Finish any pending actions mTouchInProgress = false; + touchTransformer.clearTouchingOrientation(); if (mDestroyPending) { destroy(); } } if (mInputConsumer != null) { - SimpleOrientationTouchTransformer.INSTANCE.get(mContext).transform(ev, - mRotationSupplier.get()); + if (needTransform) { + touchTransformer.transform(ev, viewRotation); + } mInputConsumer.onMotionEvent(ev); } diff --git a/quickstep/src/com/android/quickstep/util/InputProxyHandlerFactory.java b/quickstep/src/com/android/quickstep/util/InputProxyHandlerFactory.java index 843619de78..9aded898dc 100644 --- a/quickstep/src/com/android/quickstep/util/InputProxyHandlerFactory.java +++ b/quickstep/src/com/android/quickstep/util/InputProxyHandlerFactory.java @@ -35,8 +35,8 @@ public class InputProxyHandlerFactory implements Supplier { private final GestureState mGestureState; @UiThread - public InputProxyHandlerFactory(BaseContainerInterface activityInterface, - GestureState gestureState) { + public InputProxyHandlerFactory( + BaseContainerInterface activityInterface, GestureState gestureState) { mContainerInterface = activityInterface; mGestureState = gestureState; } @@ -47,7 +47,8 @@ public class InputProxyHandlerFactory implements Supplier { @Override public InputConsumer get() { RecentsViewContainer container = mContainerInterface.getCreatedContainer(); - return container == null ? InputConsumer.NO_OP + return container == null + ? InputConsumer.createNoOpInputConsumer(mGestureState.getDisplayId()) : new OverviewInputConsumer(mGestureState, container, null, true); } } diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java index 76c729aa24..e197a93093 100644 --- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java +++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java @@ -31,7 +31,6 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.Hotseat; import com.android.launcher3.Workspace; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.HorizontalInsettableView; import com.android.quickstep.SystemUiProxy; @@ -80,17 +79,12 @@ public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeL @UnfoldMain RotationChangeProvider rotationChangeProvider) { mLauncher = launcher; - if (FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { - mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider( - unfoldTransitionProgressProvider, launcher.getMainThreadHandler()); - mPreemptiveProgressProvider.init(); + mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider( + unfoldTransitionProgressProvider, launcher.getMainThreadHandler()); + mPreemptiveProgressProvider.init(); - mProgressProvider = new ScopedUnfoldTransitionProgressProvider( - mPreemptiveProgressProvider); - } else { - mProgressProvider = new ScopedUnfoldTransitionProgressProvider( - unfoldTransitionProgressProvider); - } + mProgressProvider = new ScopedUnfoldTransitionProgressProvider( + mPreemptiveProgressProvider); unfoldTransitionProgressProvider.addCallback(mExternalTransitionStatusProvider); unfoldTransitionProgressProvider.addCallback( @@ -169,11 +163,7 @@ public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeL @Override public void onDeviceProfileChanged(DeviceProfile dp) { - if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { - return; - } - - if (mIsTablet != null && dp.isTablet != mIsTablet) { + if (mIsTablet != null && dp.getDeviceProperties().isTablet() != mIsTablet) { // We should preemptively start the animation only if: // - We changed to the unfolded screen // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't @@ -183,7 +173,7 @@ public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeL // if Launcher was not open during unfold, in this case we receive the configuration // change only after we went back to home screen and we don't want to start the // animation in this case. - if (dp.isTablet + if (dp.getDeviceProperties().isTablet() && SystemUiProxy.INSTANCE.get(mLauncher).isActive() && !mExternalTransitionStatusProvider.hasRun()) { // Preemptively start the unfold animation to make sure that we have drawn @@ -191,12 +181,12 @@ public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeL preemptivelyStartAnimationOnNextFrame(); } - if (!dp.isTablet) { + if (!dp.getDeviceProperties().isTablet()) { mExternalTransitionStatusProvider.onFolded(); } } - mIsTablet = dp.isTablet; + mIsTablet = dp.getDeviceProperties().isTablet(); } private class QsbAnimationListener implements TransitionProgressListener { diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java index ec1eeb1d6b..70cbd26122 100644 --- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java +++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java @@ -23,27 +23,33 @@ import android.view.ViewGroup; import com.android.launcher3.DeviceProfile; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.NavigationMode; -import com.android.quickstep.LauncherActivityInterface; +import com.android.quickstep.BaseContainerInterface; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; public class LayoutUtils { + private static final float SQUARE_ASPECT_RATIO_TOLERANCE = 0.05f; + /** * The height for the swipe up motion */ public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) { - float swipeHeight = dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx; + float swipeHeight = dp.getAllAppsProfile().getCellHeightPx() + - dp.getAllAppsProfile().getIconTextSizePx(); if (DisplayController.getNavigationMode(context) == NavigationMode.NO_BUTTON) { swipeHeight -= dp.getInsets().bottom; } return swipeHeight; } - public static int getShelfTrackingDistance(Context context, DeviceProfile dp, - RecentsPagedOrientationHandler orientationHandler) { + public static int getShelfTrackingDistance( + Context context, + DeviceProfile dp, + RecentsPagedOrientationHandler orientationHandler, + BaseContainerInterface baseContainerInterface) { // Track the bottom of the window. Rect taskSize = new Rect(); - LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize, + baseContainerInterface.calculateTaskSize(context, dp, taskSize, orientationHandler); return orientationHandler.getDistanceToBottomOfRect(dp, taskSize); } @@ -61,4 +67,13 @@ public class LayoutUtils { } } } + + /** + * Returns true iff the device's aspect ratio is within + * {@link LayoutUtils#SQUARE_ASPECT_RATIO_TOLERANCE} of 1:1 + */ + public static boolean isAspectRatioSquare(float aspectRatio) { + return Float.compare(aspectRatio, 1f - SQUARE_ASPECT_RATIO_TOLERANCE) >= 0 + && Float.compare(aspectRatio, 1f + SQUARE_ASPECT_RATIO_TOLERANCE) <= 0; + } } diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java index b8bc828ec3..623bc5330b 100644 --- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java +++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java @@ -39,6 +39,7 @@ public class MotionPauseDetector { // The percentage of the previous speed that determines whether this is a rapid deceleration. // The bigger this number, the easier it is to trigger the first pause. private static final float RAPID_DECELERATION_FACTOR = 0.6f; + private static final float RAPID_DECELERATION_FACTOR_TRACKPAD = 0.85f; /** If no motion is added for this amount of time, assume the motion has paused. */ private static final long FORCE_PAUSE_TIMEOUT = 300; @@ -57,6 +58,7 @@ public class MotionPauseDetector { private final float mSpeedVerySlow; private final float mSpeedSlow; private final float mSpeedSomewhatFast; + private final float mSpeedTrackpadSomewhatFast; private final float mSpeedFast; private final Alarm mForcePauseTimeout; private final boolean mMakePauseHarderToTrigger; @@ -66,6 +68,7 @@ public class MotionPauseDetector { private Float mPreviousVelocity = null; private OnMotionPauseListener mOnMotionPauseListener; + private boolean mIsTrackpadGesture; private boolean mIsPaused; // Bias more for the first pause to make it feel extra responsive. private boolean mHasEverBeenPaused; @@ -94,13 +97,13 @@ public class MotionPauseDetector { mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow); mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow); mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast); + mSpeedTrackpadSomewhatFast = res.getDimension( + R.dimen.motion_pause_detector_speed_trackpad_somewhat_fast); mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast); mForcePauseTimeout = new Alarm(); mForcePauseTimeout.setOnAlarmListener(alarm -> { - ActiveGestureLog.CompoundString log = - new ActiveGestureLog.CompoundString("Force pause timeout after ") - .append(alarm.getLastSetTimeout()) - .append("ms"); + ActiveGestureLog.CompoundString log = new ActiveGestureLog.CompoundString( + "Force pause timeout after %dms", alarm.getLastSetTimeout()); addLogs(log); updatePaused(true /* isPaused */, log); }); @@ -115,13 +118,16 @@ public class MotionPauseDetector { mOnMotionPauseListener = listener; } + public void setIsTrackpadGesture(boolean isTrackpadGesture) { + mIsTrackpadGesture = isTrackpadGesture; + } + /** * @param disallowPause If true, we will not detect any pauses until this is set to false again. */ public void setDisallowPause(boolean disallowPause) { - ActiveGestureLog.CompoundString log = - new ActiveGestureLog.CompoundString("Set disallowPause=") - .append(disallowPause); + ActiveGestureLog.CompoundString log = new ActiveGestureLog.CompoundString( + "Set disallowPause=%b", disallowPause); if (mDisallowPause != disallowPause) { addLogs(log); } @@ -179,11 +185,14 @@ public class MotionPauseDetector { // We want to be more aggressive about detecting the first pause to ensure it // feels as responsive as possible; getting two very slow speeds back to back // takes too long, so also check for a rapid deceleration. - boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR; - isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast; + boolean isRapidDeceleration = + speed < previousSpeed * getRapidDecelerationFactor(); + boolean notSuperFast = speed < mSpeedSomewhatFast + || (mIsTrackpadGesture && speed < mSpeedTrackpadSomewhatFast); + isPaused = isRapidDeceleration && notSuperFast; isPausedReason = new ActiveGestureLog.CompoundString( - "Didn't have back to back slow speeds, checking for rapid ") - .append(" deceleration on first pause only"); + "Didn't have back to back slow speeds, checking for rapid " + + " deceleration on first pause only"); } if (mMakePauseHarderToTrigger) { if (speed < mSpeedSlow) { @@ -192,8 +201,8 @@ public class MotionPauseDetector { } isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT; isPausedReason = new ActiveGestureLog.CompoundString( - "Maintained slow speed for sufficient duration when making") - .append(" pause harder to trigger"); + "Maintained slow speed for sufficient duration when making" + + " pause harder to trigger"); } else { mSlowStartTime = 0; isPaused = false; @@ -209,17 +218,14 @@ public class MotionPauseDetector { private void updatePaused(boolean isPaused, ActiveGestureLog.CompoundString reason) { if (mDisallowPause) { reason = new ActiveGestureLog.CompoundString( - "Disallow pause; otherwise, would have been ") - .append(isPaused) - .append(" due to reason:") + "Disallow pause; otherwise, would have been %b due to reason: ", isPaused) .append(reason); isPaused = false; } if (mIsPaused != isPaused) { mIsPaused = isPaused; - addLogs(new ActiveGestureLog.CompoundString("onMotionPauseChanged triggered; paused=") - .append(mIsPaused) - .append(", reason=") + addLogs(new ActiveGestureLog.CompoundString( + "onMotionPauseChanged triggered; paused=%b, reason=", mIsPaused) .append(reason)); boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused; if (mIsPaused) { @@ -239,20 +245,20 @@ public class MotionPauseDetector { } } - private void addLogs(ActiveGestureLog.CompoundString compoundString) { - ActiveGestureLog.CompoundString logString = - new ActiveGestureLog.CompoundString("MotionPauseDetector: ") - .append(compoundString); + private void addLogs(ActiveGestureLog.CompoundString event) { if (Utilities.isRunningInTestHarness()) { - Log.d(TAG, logString.toString()); + Log.d(TAG, new ActiveGestureLog.CompoundString("MotionPauseDetector: ") + .append(event) + .toString()); } - ActiveGestureLog.INSTANCE.addLog(logString); + ActiveGestureProtoLogProxy.logMotionPauseDetectorEvent(event); } public void clear() { mVelocityProvider.clear(); mPreviousVelocity = null; setOnMotionPauseListener(null); + mIsTrackpadGesture = false; mIsPaused = mHasEverBeenPaused = false; mSlowStartTime = 0; mForcePauseTimeout.cancelAlarm(); @@ -262,6 +268,13 @@ public class MotionPauseDetector { return mIsPaused; } + private float getRapidDecelerationFactor() { + return mIsTrackpadGesture ? Float.parseFloat( + Utilities.getSystemProperty("trackpad_in_app_swipe_up_deceleration_factor", + String.valueOf(RAPID_DECELERATION_FACTOR_TRACKPAD))) + : RAPID_DECELERATION_FACTOR; + } + public interface OnMotionPauseListener { /** Called only the first time motion pause is detected. */ void onMotionPauseDetected(); diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java index cfe5b2cd61..a6da2e9e02 100644 --- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java +++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java @@ -82,7 +82,7 @@ public class QuickstepOnboardingPrefs { public void onStateTransitionComplete(LauncherState finalState) { HotseatPredictionController client = launcher.getHotseatPredictionController(); if (mFromAllApps && finalState == NORMAL && client.hasPredictions()) { - if (!launcher.getDeviceProfile().isTablet + if (!launcher.getDeviceProfile().getDeviceProperties().isTablet() && HOTSEAT_DISCOVERY_TIP_COUNT.increment(launcher)) { client.showEdu(); stateManager.removeStateListener(this); @@ -106,7 +106,8 @@ public class QuickstepOnboardingPrefs { return; } mShouldIncreaseCount = toState == HINT_STATE - && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE; + && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE + && launcher.isCanShowAllAppsEducationView(); } @Override diff --git a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java index 0b05c2e7c1..63fe017023 100644 --- a/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java +++ b/quickstep/src/com/android/quickstep/util/RecentsAtomicAnimationFactory.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET; +import static com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA; import android.animation.Animator; import android.animation.ObjectAnimator; @@ -33,8 +34,10 @@ public class RecentsAtomicAnimationFactory bottomThreshold) { if (enableScalingRevealHomeAnimation()) { diff --git a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt index f547a7fba1..9928b35acb 100644 --- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt +++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt @@ -16,19 +16,30 @@ package com.android.quickstep.util +import android.animation.Animator +import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet +import android.animation.ValueAnimator import android.graphics.Matrix import android.graphics.Path import android.graphics.RectF +import android.util.Log +import android.view.SurfaceControl import android.view.View import android.view.animation.PathInterpolator import androidx.core.graphics.transform +import androidx.core.view.isVisible +import com.android.app.animation.Animations import com.android.app.animation.Interpolators +import com.android.app.animation.Interpolators.EMPHASIZED import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.Flags import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE +import com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY import com.android.launcher3.LauncherState +import com.android.launcher3.R import com.android.launcher3.anim.AnimatorListeners import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.anim.PropertySetter @@ -39,28 +50,32 @@ import com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM import com.android.launcher3.uioverrides.QuickstepLauncher import com.android.quickstep.views.RecentsView +const val TAG = "ScalingWorkspaceRevealAnim" + /** * Creates an animation where the workspace and hotseat fade in while revealing from the center of * the screen outwards radially. This is used in conjunction with the swipe up to home animation. */ class ScalingWorkspaceRevealAnim( - launcher: QuickstepLauncher, + private val launcher: QuickstepLauncher, siblingAnimation: RectFSpringAnim?, - windowTargetRect: RectF? + windowTargetRect: RectF?, + playAlphaReveal: Boolean = true, ) { companion object { private const val FADE_DURATION_MS = 200L private const val SCALE_DURATION_MS = 1000L private const val MAX_ALPHA = 1f private const val MIN_ALPHA = 0f - private const val MAX_SIZE = 1f - private const val MIN_SIZE = 0.85f + internal const val MAX_SIZE = 1f + internal const val MIN_SIZE = 0.85f /** * Custom interpolator for both the home and wallpaper scaling. Necessary because EMPHASIZED * is too aggressive, but EMPHASIZED_DECELERATE is too soft. */ - private val SCALE_INTERPOLATOR = + @JvmField + val SCALE_INTERPOLATOR = PathInterpolator( Path().apply { moveTo(0f, 0f) @@ -68,9 +83,14 @@ class ScalingWorkspaceRevealAnim( cubicTo(0.235f, 0.6855f, 0.235f, 1f, 1f, 1f) } ) + + val BLUR_INTERPOLATOR = Interpolators.clampToProgress(EMPHASIZED, 0f, 0.666f) } private val animation = PendingAnimation(SCALE_DURATION_MS) + private var blurLayer: SurfaceControl? = null + private var surfaceTransactionApplier: SurfaceTransactionApplier = + SurfaceTransactionApplier(launcher.dragLayer) init { // Make sure the starting state is right for the animation. @@ -86,58 +106,96 @@ class ScalingWorkspaceRevealAnim( launcher.workspace.stateTransitionAnimation.setScrim( PropertySetter.NO_ANIM_PROPERTY_SETTER, LauncherState.BACKGROUND_APP, - setupConfig + setupConfig, ) + addBlurLayer() val workspace = launcher.workspace val hotseat = launcher.hotseat + // Interrupt the current animation, if any. + Animations.cancelOngoingAnimation(workspace) + Animations.cancelOngoingAnimation(hotseat) + + val fromSize = + if (workspace.scaleX != MAX_SIZE) { + workspace.scaleX + } else { + MIN_SIZE + } + // Scale the Workspace and Hotseat around the same pivot. workspace.setPivotToScaleWithSelf(hotseat) animation.addFloat( workspace, WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE], - MIN_SIZE, + fromSize, MAX_SIZE, SCALE_INTERPOLATOR, ) animation.addFloat( hotseat, HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE], - MIN_SIZE, + fromSize, MAX_SIZE, SCALE_INTERPOLATOR, ) - // Fade in quickly at the beginning of the animation, so the content doesn't look like it's - // popping into existence out of nowhere. - val fadeClamp = FADE_DURATION_MS.toFloat() / SCALE_DURATION_MS - workspace.alpha = MIN_ALPHA - animation.setViewAlpha( - workspace, - MAX_ALPHA, - Interpolators.clampToProgress(LINEAR, 0f, fadeClamp) - ) - hotseat.alpha = MIN_ALPHA - animation.setViewAlpha( - hotseat, - MAX_ALPHA, - Interpolators.clampToProgress(LINEAR, 0f, fadeClamp) - ) + if (playAlphaReveal) { + // Fade in quickly at the beginning of the animation, so the content doesn't look like + // it's popping into existence out of nowhere. + val fadeClamp = FADE_DURATION_MS.toFloat() / SCALE_DURATION_MS + workspace.alpha = MIN_ALPHA + animation.setFloat( + workspace, + VIEW_ALPHA, + MAX_ALPHA, + Interpolators.clampToProgress(LINEAR, 0f, fadeClamp), + ) + hotseat.alpha = MIN_ALPHA + // This needs to use setViewAlpha instead of setFloat (like workspace). + // This is because hotseat visibility can also be changed based off of alpha in + // WorkspaceRevealAnim which also calls setViewAlpha. + // b/428257480 Ideally we should be settings MultiValueAlpha with 2 channels instead. + animation.setViewAlpha( + hotseat, + MAX_ALPHA, + Interpolators.clampToProgress(LINEAR, 0f, fadeClamp), + ) + } val transitionConfig = StateAnimationConfig() + transitionConfig.duration = SCALE_DURATION_MS - // Match the Wallpaper animation to the rest of the content. + // Match the Wallpaper depth to the rest of the content. val depthController = (launcher as? QuickstepLauncher)?.depthController transitionConfig.setInterpolator(StateAnimationConfig.ANIM_DEPTH, SCALE_INTERPOLATOR) + depthController?.pauseBlursOnWindows(true) // Blurring is handled by the scrim layer. + depthController?.stateDepth?.value = LauncherState.BACKGROUND_APP.getDepth(launcher) depthController?.setStateWithAnimation(LauncherState.NORMAL, transitionConfig, animation) - // Make sure that the contrast scrim animates correctly if needed. - transitionConfig.setInterpolator(StateAnimationConfig.ANIM_SCRIM_FADE, SCALE_INTERPOLATOR) + // Add a blur animation to the scrim layer. + var maxBlurRadius = + launcher.resources.getDimensionPixelSize( + if (Flags.allAppsBlur() || Flags.enableOverviewBackgroundWallpaperBlur()) { + R.dimen.max_depth_blur_radius_enhanced + } else { + R.integer.max_depth_blur_radius + } + ) + val blurAnimator = ValueAnimator.ofFloat(1f, 0f) + blurAnimator.setInterpolator(BLUR_INTERPOLATOR) + blurAnimator.addUpdateListener { + applyBlur(maxBlurRadius * blurAnimator.animatedValue as Float) + } + animation.add(blurAnimator) + + // Make sure that the contrast scrim animates correctly (alongside the blur) if needed. + transitionConfig.setInterpolator(StateAnimationConfig.ANIM_SCRIM_FADE, BLUR_INTERPOLATOR) launcher.workspace.stateTransitionAnimation.setScrim( animation, LauncherState.NORMAL, - transitionConfig + transitionConfig, ) // To avoid awkward jumps in icon position, we want the sibling animation to always be @@ -164,7 +222,7 @@ class ScalingWorkspaceRevealAnim( 1 / workspace.scaleX, 1 / workspace.scaleY, transformed.centerX(), - transformed.centerY() + transformed.centerY(), ) } ) @@ -178,11 +236,49 @@ class ScalingWorkspaceRevealAnim( // Needed to avoid text artefacts during the scale animation. workspace.setLayerType(View.LAYER_TYPE_HARDWARE, null) hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null) + animation.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationCancel(animation: Animator) { + super.onAnimationCancel(animation) + Log.d(TAG, "onAnimationCancel") + } + + override fun onAnimationPause(animation: Animator) { + super.onAnimationPause(animation) + Log.d(TAG, "onAnimationPause") + } + } + ) + animation.addListener( AnimatorListeners.forEndCallback( Runnable { + Log.d(TAG, "onAnimationEnd, workspace and hotseat are visible") + // Ensure that the workspace and the hotseat are visible at the end + // of the animation regardless of what happens with this animation + // itself. + workspace.alpha = MAX_ALPHA + hotseat.alpha = MAX_ALPHA + if (!hotseat.isVisible || !workspace.isVisible) { + Log.e( + TAG, + "Unexpected invisibility after animation end:" + + " workspace.isVisible=${workspace.isVisible}" + + ", workspace.alpha=${workspace.alpha}" + + ", hotseat.isVisible=${hotseat.isVisible}" + + ", hotseat.alpha=${hotseat.alpha}", + Exception(), + ) + } + workspace.setLayerType(View.LAYER_TYPE_NONE, null) hotseat.setLayerType(View.LAYER_TYPE_NONE, null) + + // Reset the cached animations. + Animations.setOngoingAnimation(workspace, animation = null) + Animations.setOngoingAnimation(hotseat, animation = null) + removeBlurLayer() + depthController?.pauseBlursOnWindows(false) } ) ) @@ -193,6 +289,59 @@ class ScalingWorkspaceRevealAnim( } fun start() { - getAnimators().start() + val animators = getAnimators() + // Make sure to cache the current animation, so it can be properly interrupted. + // TODO(b/367591368): ideally these animations would be refactored to be controlled + // centrally so each instances doesn't need to care about this coordination. + Animations.setOngoingAnimation(launcher.workspace, animators) + Animations.setOngoingAnimation(launcher.hotseat, animators) + launcher.stateManager.setCurrentAnimation(animators, LauncherState.NORMAL) + animators.start() + } + + private fun addBlurLayer() { + val parent = launcher.dragLayer.viewRootImpl?.surfaceControl ?: return + if (!parent.isValid) { + Log.e(TAG, "Parent surface is not ready at the moment. Can't apply blur.") + return + } + val blurLayer = + SurfaceControl.Builder() + .setName("Home to launcher blur layer") + .setCallsite("ScalingWorkspaceRevealAnim") + .setParent(parent) + .setOpaque(false) + .setHidden(false) + .build() + + // Schedule the initial setup of the blur layer. + val setupTransaction = SurfaceTransaction() + setupTransaction.forSurface(blurLayer).setAlpha(0f).setShow() + surfaceTransactionApplier.scheduleApply(setupTransaction) + + this.blurLayer = blurLayer + } + + private fun removeBlurLayer() { + blurLayer?.let { + if (it.isValid) { + // Schedule the removal of the blur layer. + val removalTransaction = SurfaceTransaction() + removalTransaction.forSurface(it).setRemove() + surfaceTransactionApplier.scheduleApply(removalTransaction) + } + } + blurLayer = null + } + + private fun applyBlur(blurRadius: Float) { + blurLayer?.let { + if (it.isValid) { + // Schedule the blur update. + val blurUpdateTransaction = SurfaceTransaction() + blurUpdateTransaction.forSurface(it).setBackgroundBlurRadius(blurRadius.toInt()) + surfaceTransactionApplier.scheduleApply(blurUpdateTransaction) + } + } } } diff --git a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt index dbeedd33f7..15f59e425f 100644 --- a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt +++ b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt @@ -43,7 +43,7 @@ class SlideInRemoteTransition( transition: IBinder, info: TransitionInfo, startT: Transaction, - finishCB: IRemoteTransitionFinishedCallback + finishCB: IRemoteTransitionFinishedCallback, ) { val anim = ValueAnimator.ofFloat(0f, 1f) anim.interpolator = interpolator diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 4863a6b2da..55644bd3f3 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -27,39 +27,46 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.Context import android.graphics.Bitmap +import android.graphics.Color import android.graphics.Rect import android.graphics.RectF +import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.view.RemoteAnimationTarget import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.View +import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.WindowContainerToken import androidx.annotation.VisibleForTesting +import androidx.core.util.component1 +import androidx.core.util.component2 import com.android.app.animation.Interpolators import com.android.launcher3.DeviceProfile -import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.InsettableFrameLayout import com.android.launcher3.QuickstepTransitionManager import com.android.launcher3.R import com.android.launcher3.Utilities +import com.android.launcher3.anim.AnimatedFloat import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.apppairs.AppPairIcon -import com.android.launcher3.config.FeatureFlags import com.android.launcher3.logging.StatsLogManager.EventEnum import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager import com.android.launcher3.taskbar.TaskbarActivityContext -import com.android.launcher3.uioverrides.QuickstepLauncher import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource +import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.BaseDragLayer import com.android.quickstep.TaskViewUtils +import com.android.quickstep.util.SplitScreenUtils.Companion.extractTopParentAndChildren import com.android.quickstep.views.FloatingAppPairView import com.android.quickstep.views.FloatingTaskView import com.android.quickstep.views.GroupedTaskView @@ -67,9 +74,9 @@ import com.android.quickstep.views.IconAppChipView import com.android.quickstep.views.RecentsView import com.android.quickstep.views.RecentsViewContainer import com.android.quickstep.views.SplitInstructionsView +import com.android.quickstep.views.TaskContainer import com.android.quickstep.views.TaskThumbnailViewDeprecated import com.android.quickstep.views.TaskView -import com.android.quickstep.views.TaskView.TaskContainer import com.android.quickstep.views.TaskViewIcon import com.android.wm.shell.shared.TransitionUtil import java.util.Optional @@ -88,7 +95,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC val iconDrawable: Drawable, val fadeWithThumbnail: Boolean, val isStagedTask: Boolean, - val iconView: View? + val iconView: View?, + val contentDescription: CharSequence?, ) } @@ -98,7 +106,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC */ fun getFirstAnimInitViews( taskViewSupplier: Supplier, - splitSelectSourceSupplier: Supplier + splitSelectSourceSupplier: Supplier, ): SplitAnimInitProps { val splitSelectSource = splitSelectSourceSupplier.get() if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { @@ -109,7 +117,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, - iconView = null + iconView = null, + splitSelectSource.itemInfo.contentDescription, ) } else if (splitSelectStateController.isDismissingFromSplitPair) { // Initiating split from overview, but on a split pair @@ -118,31 +127,33 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) { val drawable = getDrawable(container.iconView, splitSelectSource) return SplitAnimInitProps( - container.thumbnailViewDeprecated, - container.thumbnailViewDeprecated.thumbnail, - drawable!!, + container.snapshotView, + container.thumbnail, + drawable, fadeWithThumbnail = true, isStagedTask = true, - iconView = container.iconView.asView() + iconView = container.iconView.asView(), + container.task.titleDescription, ) } } throw IllegalStateException( "Attempting to init split from existing split pair " + - "without a valid taskIdAttributeContainer" + "without a valid taskIdAttributeContainer" ) } else { // Initiating split from overview on fullscreen task TaskView val taskView = taskViewSupplier.get() - taskView.taskContainers.first().let { + taskView.firstTaskContainer!!.let { val drawable = getDrawable(it.iconView, splitSelectSource) return SplitAnimInitProps( - it.thumbnailViewDeprecated, - it.thumbnailViewDeprecated.thumbnail, - drawable!!, + it.snapshotView, + it.thumbnail, + drawable, fadeWithThumbnail = true, isStagedTask = true, - iconView = it.iconView.asView() + iconView = it.iconView.asView(), + it.task.titleDescription, ) } } @@ -151,64 +162,90 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC /** * Returns the drawable that's provided in iconView, however if that is null it falls back to * the drawable that's in splitSelectSource. TaskView's icon drawable can be null if the - * TaskView is scrolled far enough off screen + * TaskView is scrolled far enough off screen. * - * @return [Drawable] + * @return the [Drawable] icon, or a translucent drawable if none was found */ - fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable? { - if (iconView.drawable == null && splitSelectSource != null) { - return splitSelectSource.drawable - } - return iconView.drawable + fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable { + val drawable = + if (iconView.drawable == null && splitSelectSource != null) splitSelectSource.drawable + else iconView.drawable + return drawable ?: ColorDrawable(Color.TRANSPARENT) } /** * When selecting first app from split pair, second app's thumbnail remains. This animates the * second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying it - * with [TaskThumbnailViewDeprecated]'s splashView. Adds animations to the provided builder. - * Note: The app that **was not** selected as the first split app should be the container that's - * passed through. + * with [TaskContainer]'s splashView. Adds animations to the provided builder. Note: The app + * that **was not** selected as the first split app should be the container that's passed + * through. * * @param builder Adds animation to this - * @param taskIdAttributeContainer container of the app that **was not** selected + * @param taskContainer container of the app that **was not** selected * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair - * (opposite of that representing [taskIdAttributeContainer]) + * (opposite of that representing [taskContainer]) */ fun addInitialSplitFromPair( - taskIdAttributeContainer: TaskContainer, + taskContainer: TaskContainer, builder: PendingAnimation, deviceProfile: DeviceProfile, taskViewWidth: Int, taskViewHeight: Int, - isPrimaryTaskSplitting: Boolean + isPrimaryTaskSplitting: Boolean, ) { - val thumbnail = taskIdAttributeContainer.thumbnailViewDeprecated - val iconView: View = taskIdAttributeContainer.iconView.asView() - builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailViewDeprecated.SPLASH_ALPHA, 1f)) - thumbnail.setShowSplashForSplitSelection(true) + val taskContentView = taskContainer.taskContentView + val iconView: View = taskContainer.iconView.asView() + if (enableRefactorTaskThumbnail()) { + builder.add( + AnimatedFloat { v -> taskContainer.taskView.splitSplashAlpha = v } + .animateToValue(1f) + ) + } else { + val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated + builder.add( + ObjectAnimator.ofFloat( + thumbnailViewDeprecated, + TaskThumbnailViewDeprecated.SPLASH_ALPHA, + 1f, + ) + ) + thumbnailViewDeprecated.setShowSplashForSplitSelection(true) + } // With the new `IconAppChipView`, we always want to keep the chip pinned to the // top left of the task / thumbnail. if (enableOverviewIconMenu()) { builder.add( ObjectAnimator.ofFloat( - (iconView as IconAppChipView).splitTranslationX, + (iconView as IconAppChipView).getSplitTranslationX(), MULTI_PROPERTY_VALUE, - 0f + 0f, ) ) builder.add( - ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f) + ObjectAnimator.ofFloat(iconView.getSplitTranslationY(), MULTI_PROPERTY_VALUE, 0f) ) } + + val splitBoundsConfig = + (taskContainer.taskView as? GroupedTaskView)?.splitBoundsConfig ?: return + val (primarySnapshotViewSize, secondarySnapshotViewSize) = + taskContainer.taskView.pagedOrientationHandler.getGroupedTaskViewSizes( + deviceProfile, + splitBoundsConfig, + taskViewWidth, + taskViewHeight, + ) + val snapshotViewSize = + if (isPrimaryTaskSplitting) secondarySnapshotViewSize else primarySnapshotViewSize if (deviceProfile.isLeftRightSplit) { // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 - val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f - val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width + val centerThumbnailTranslationX: Float = (taskViewWidth - snapshotViewSize.x) / 2f + val finalScaleX: Float = taskViewWidth.toFloat() / snapshotViewSize.x builder.add( ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, - centerThumbnailTranslationX + taskContentView, + View.TRANSLATION_X, + centerThumbnailTranslationX, ) ) if (!enableOverviewIconMenu()) { @@ -218,23 +255,20 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX) ) } - builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) + builder.add(ObjectAnimator.ofFloat(taskContentView, View.SCALE_X, finalScaleX)) // Reset other dimensions // TODO(b/271468547), can't set Y translate to 0, need to account for top space - thumbnail.scaleY = 1f + taskContentView.scaleY = 1f val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f - else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() + else deviceProfile.overviewProfile.taskThumbnailTopMarginPx.toFloat() builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, - translateYResetVal - ) + ObjectAnimator.ofFloat(taskContentView, View.TRANSLATION_Y, translateYResetVal) ) } else { - val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx + val thumbnailSize = + taskViewHeight - deviceProfile.overviewProfile.taskThumbnailTopMarginPx // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 // primary thumbnail has layout margin above it, so secondary thumbnail needs to take // that into account. We should migrate to only using translations otherwise this @@ -247,18 +281,18 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // thumbnail needs to take that into account. We should migrate to only using // translations otherwise this asymmetry causes problems.. if (isPrimaryTaskSplitting) { - centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + centerThumbnailTranslationY = (thumbnailSize - snapshotViewSize.y) / 2f centerThumbnailTranslationY += - deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() + deviceProfile.overviewProfile.taskThumbnailTopMarginPx.toFloat() } else { - centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f + centerThumbnailTranslationY = (thumbnailSize - snapshotViewSize.y) / 2f } - val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height + val finalScaleY: Float = thumbnailSize.toFloat() / snapshotViewSize.y builder.add( ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, - centerThumbnailTranslationY + taskContentView, + View.TRANSLATION_Y, + centerThumbnailTranslationY, ) ) @@ -266,17 +300,11 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // icons are anchored from Gravity.END, so need to use negative translation builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f)) } - builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) + builder.add(ObjectAnimator.ofFloat(taskContentView, View.SCALE_Y, finalScaleY)) // Reset other dimensions - thumbnail.scaleX = 1f - builder.add( - ObjectAnimator.ofFloat( - thumbnail, - TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, - 0f - ) - ) + taskContentView.scaleX = 1f + builder.add(ObjectAnimator.ofFloat(taskContentView, View.TRANSLATION_X, 0f)) } } @@ -287,7 +315,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC fun addScrimBehindAnim( pendingAnimation: PendingAnimation, container: RecentsViewContainer, - context: Context + context: Context, ): View { val scrim = View(context) val recentsView = container.getOverviewPanel>() @@ -301,22 +329,24 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // Make the scrim fullscreen val lp = scrim.layoutParams as InsettableFrameLayout.LayoutParams lp.topMargin = 0 - lp.height = dp.heightPx - lp.width = dp.widthPx + lp.height = dp.deviceProperties.heightPx + lp.width = dp.deviceProperties.widthPx scrim.alpha = 0f scrim.setBackgroundColor( container.asContext().resources.getColor(R.color.taskbar_background_dark) ) - val timings = AnimUtils.getDeviceSplitToConfirmTimings(dp.isTablet) as SplitToConfirmTimings + val timings = + AnimUtils.getDeviceSplitToConfirmTimings(dp.deviceProperties.isTablet) + as SplitToConfirmTimings pendingAnimation.setViewAlpha( scrim, 1f, Interpolators.clampToProgress( timings.backingScrimFadeInterpolator, timings.backingScrimFadeInStartOffset, - timings.backingScrimFadeInEndOffset - ) + timings.backingScrimFadeInEndOffset, + ), ) return scrim @@ -339,7 +369,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC fun createPlaceholderDismissAnim( container: RecentsViewContainer, splitDismissEvent: EventEnum, - duration: Long? + duration: Long?, ): AnimatorSet { val animatorSet = AnimatorSet() duration?.let { animatorSet.duration = it } @@ -356,7 +386,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Rect(0, 0, floatingTask.width, floatingTask.height), false, null, - onScreenRectF + onScreenRectF, ) // Get the part of the floatingTask that intersects with the DragLayer (i.e. the // on-screen portion) @@ -364,7 +394,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC dragLayer.left.toFloat(), dragLayer.top.toFloat(), dragLayer.right.toFloat(), - dragLayer.bottom.toFloat() + dragLayer.bottom.toFloat(), ) animatorSet.play( ObjectAnimator.ofFloat( @@ -374,8 +404,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC floatingTask, onScreenRectF, floatingTask.stagePosition, - container.deviceProfile - ) + container.deviceProfile, + ), ) ) animatorSet.addListener( @@ -384,7 +414,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC splitSelectStateController.resetState() safeRemoveViewFromDragLayer( container, - splitSelectStateController.splitInstructionsView + splitSelectStateController.splitInstructionsView, ) } } @@ -401,7 +431,10 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC safeRemoveViewFromDragLayer(container, splitSelectStateController.splitInstructionsView) val splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(container) splitSelectStateController.splitInstructionsView = splitInstructionsView - val timings = AnimUtils.getDeviceOverviewToSplitTimings(container.deviceProfile.isTablet) + val timings = + AnimUtils.getDeviceOverviewToSplitTimings( + container.deviceProfile.deviceProperties.isTablet + ) val anim = PendingAnimation(100 /*duration */) splitInstructionsView.alpha = 0f anim.setViewAlpha( @@ -410,8 +443,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.clampToProgress( Interpolators.LINEAR, timings.instructionsContainerFadeInStartOffset, - timings.instructionsContainerFadeInEndOffset - ) + timings.instructionsContainerFadeInEndOffset, + ), ) anim.addFloat( splitInstructionsView, @@ -421,8 +454,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.clampToProgress( Interpolators.EMPHASIZED_DECELERATE, timings.instructionsUnfoldStartOffset, - timings.instructionsUnfoldEndOffset - ) + timings.instructionsUnfoldEndOffset, + ), ) return anim } @@ -440,11 +473,11 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC fun playAnimPlaceholderToFullscreen( container: RecentsViewContainer, view: View, - resetCallback: Optional + resetCallback: Optional, ) { val stagedTaskView = view as FloatingTaskView - val isTablet: Boolean = container.deviceProfile.isTablet + val isTablet: Boolean = container.deviceProfile.deviceProperties.isTablet val duration = if (isTablet) SplitAnimationTimings.TABLET_CONFIRM_DURATION else SplitAnimationTimings.PHONE_CONFIRM_DURATION @@ -462,16 +495,12 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC RectF(firstTaskStartingBounds), firstTaskEndingBounds, false /* fadeWithThumbnail */, - true /* isStagedTask */ + true, /* isStagedTask */ ) pendingAnimation.addEndListener { splitSelectStateController.launchInitialAppFullscreen { - if (FeatureFlags.enableSplitContextually()) { - splitSelectStateController.resetState() - } else if (resetCallback.isPresent) { - resetCallback.get().run() - } + splitSelectStateController.resetState() } } @@ -495,14 +524,15 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC depthController: DepthController?, info: TransitionInfo?, t: Transaction?, - finishCallback: Runnable + finishCallback: Runnable, + cornerRadius: Float, ) { if (info == null && t == null) { // (Legacy animation) Tapping a split tile in Overview // TODO (b/315490678): Ensure that this works with app pairs flow check(apps != null && wallpapers != null && nonApps != null) { "trying to call composeRecentsSplitLaunchAnimatorLegacy, but encountered an " + - "unexpected null" + "unexpected null" } composeRecentsSplitLaunchAnimatorLegacy( @@ -514,7 +544,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC nonApps, stateManager, depthController, - finishCallback + finishCallback, ) return @@ -532,7 +562,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC depthController, info, t, - finishCallback + finishCallback, ) } else if (launchingIconView != null) { // Tapping an app pair icon @@ -542,24 +572,37 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info) if (appPairLaunchingAppIndex == -1) { // Launch split app pair animation - composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) + composeIconSplitLaunchAnimator( + launchingIconView, + info, + t, + finishCallback, + cornerRadius, + ) } else { composeFullscreenIconSplitLaunchAnimator( launchingIconView, info, t, finishCallback, - appPairLaunchingAppIndex + appPairLaunchingAppIndex, ) } } else { // Fallback case: simple fade-in animation check(info != null && t != null) { "trying to call composeFadeInSplitLaunchAnimator, but encountered an " + - "unexpected null" + "unexpected null" } - composeFadeInSplitLaunchAnimator(initialTaskId, secondTaskId, info, t, finishCallback) + composeFadeInSplitLaunchAnimator( + initialTaskId, + secondTaskId, + info, + t, + finishCallback, + cornerRadius, + ) } } @@ -574,7 +617,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC depthController: DepthController?, info: TransitionInfo, t: Transaction, - finishCallback: Runnable + finishCallback: Runnable, ) { TaskViewUtils.composeRecentsSplitLaunchAnimator( launchingTaskView, @@ -582,7 +625,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC depthController, info, t, - finishCallback + finishCallback, ) } @@ -600,7 +643,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC nonApps: Array, stateManager: StateManager<*, *>, depthController: DepthController?, - finishCallback: Runnable + finishCallback: Runnable, ) { TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy( launchingTaskView, @@ -611,7 +654,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC nonApps, stateManager, depthController, - finishCallback + finishCallback, ) } @@ -622,7 +665,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC */ fun hasChangesForBothAppPairs( launchingIconView: AppPairIcon, - transitionInfo: TransitionInfo + transitionInfo: TransitionInfo, ): Int { val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName @@ -631,7 +674,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC val taskInfo: RunningTaskInfo = change.taskInfo ?: continue if ( TransitionUtil.isOpeningType(change.mode) && - taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN + taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN ) { val baseIntent = taskInfo.baseIntent.component?.packageName if (baseIntent == intent1) { @@ -659,15 +702,15 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC * To find the root shell leash that we want to fade in, we do the following: The Changes we * receive in transitionInfo are structured like this * - * Root (grandparent) + * (0) Root (grandparent) * | - * |--> Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW) + * |--> (1) Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW) * | | - * | --> App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW) + * | --> (1a) App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW) * |--> Divider - * |--> Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW) + * |--> (2) Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW) * | - * --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW) + * --> (2a) App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW) * * We want to animate the Root (grandparent) so that it affects both apps and the divider. To do * this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the left-side ones, @@ -682,7 +725,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC launchingIconView: AppPairIcon, transitionInfo: TransitionInfo, t: Transaction, - finishCallback: Runnable + finishCallback: Runnable, + windowRadius: Float, ) { // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation @@ -691,59 +735,38 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo, t, finishCallback, - WINDOWING_MODE_MULTI_WINDOW + WINDOWING_MODE_MULTI_WINDOW, ) return } // Else we are in Launcher and can launch with the full icon stretch-and-split animation. - val launcher = QuickstepLauncher.getLauncher(launchingIconView.context) + val launcher: ActivityContext = ActivityContext.lookupContext(launchingIconView.context) val dp = launcher.deviceProfile // Create an AnimatorSet that will run both shell and launcher transitions together val launchAnimation = AnimatorSet() - var rootCandidate: Change? = null - for (change in transitionInfo.changes) { - val taskInfo: RunningTaskInfo = change.taskInfo ?: continue + val splitRoots: Pair>? = extractTopParentAndChildren(transitionInfo) + check(splitRoots != null) { "Could not find split roots" } - // TODO (b/316490565): Replace this logic when SplitBounds is available to - // startAnimation() and we can know the precise taskIds of launching tasks. - // Find a change that has WINDOWING_MODE_MULTI_WINDOW. - if ( - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW && - (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) - ) { - // Check if it is a left/top app. - val isLeftTopApp = - (dp.isLeftRightSplit && change.endAbsBounds.left == 0) || - (!dp.isLeftRightSplit && change.endAbsBounds.top == 0) - if (isLeftTopApp) { - // Found one! - rootCandidate = change - break - } - } - } - - // If we could not find a proper root candidate, something went wrong. - check(rootCandidate != null) { "Could not find a split root candidate" } + // Will point to change (0) in diagram above + val mainRootCandidate = splitRoots.first + // Will contain changes (1) and (2) in diagram above + val leafRoots: List = splitRoots.second + // Don't rely on DP.isLeftRightSplit because if launcher is portrait apps could still + // launch in landscape if system auto-rotate is enabled and phone is held horizontally + val isLeftRightSplit = leafRoots.all { it.endAbsBounds.top == 0 } // Find the place where our left/top app window meets the divider (used for the // launcher side animation) + val leftTopApp = + leafRoots.single { change -> + (isLeftRightSplit && change.endAbsBounds.left <= 0) || + (!isLeftRightSplit && change.endAbsBounds.top <= 0) + } val dividerPos = - if (dp.isLeftRightSplit) rootCandidate.endAbsBounds.right - else rootCandidate.endAbsBounds.bottom - - // Recurse up the tree until parent is null, then we've found our root. - var parentToken: WindowContainerToken? = rootCandidate.parent - while (parentToken != null) { - rootCandidate = transitionInfo.getChange(parentToken) ?: break - parentToken = rootCandidate.parent - } - - // Make sure nothing weird happened, like getChange() returning null. - check(rootCandidate != null) { "Failed to find a root leash" } + if (isLeftRightSplit) leftTopApp.endAbsBounds.right else leftTopApp.endAbsBounds.bottom // Create a new floating view in Launcher, positioned above the launching icon val drawableArea = launchingIconView.iconDrawableArea @@ -758,13 +781,30 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC drawableArea, appIcon1, appIcon2, - dividerPos + dividerPos, ) floatingView.bringToFront() - launchAnimation.play( - getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate) + val iconLaunchValueAnimator = + getIconLaunchValueAnimator( + t, + dp, + finishCallback, + launcher, + floatingView, + mainRootCandidate, + ) + iconLaunchValueAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator, isReverse: Boolean) { + for (c in leafRoots) { + t.setCornerRadius(c.leash, windowRadius) + t.apply() + } + } + } ) + launchAnimation.play(iconLaunchValueAnimator) launchAnimation.start() } @@ -778,7 +818,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo: TransitionInfo, t: Transaction, finishCallback: Runnable, - launchFullscreenIndex: Int + launchFullscreenIndex: Int, ) { // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation @@ -787,13 +827,13 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo, t, finishCallback, - WINDOWING_MODE_FULLSCREEN + WINDOWING_MODE_FULLSCREEN, ) return } // Else we are in Launcher and can launch with the full icon stretch-and-split animation. - val launcher = QuickstepLauncher.getLauncher(launchingIconView.context) + val launcher: ActivityContext = ActivityContext.lookupContext(launchingIconView.context) val dp = launcher.deviceProfile // Create an AnimatorSet that will run both shell and launcher transitions together @@ -808,8 +848,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC val baseIntent = taskInfo.baseIntent.component?.packageName if ( TransitionUtil.isOpeningType(change.mode) && - taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN && - baseIntent == intentToLaunch + taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN && + baseIntent == intentToLaunch ) { rootCandidate = change } @@ -839,7 +879,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC drawableArea, appIcon, null /*appIcon2*/, - 0 /*dividerPos*/ + 0, /*dividerPos*/ ) floatingView.bringToFront() launchAnimation.play( @@ -850,14 +890,14 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC private fun getIconLaunchValueAnimator( t: Transaction, - dp: com.android.launcher3.DeviceProfile, + dp: DeviceProfile, finishCallback: Runnable, - launcher: QuickstepLauncher, + launcher: ActivityContext, floatingView: FloatingAppPairView, - rootCandidate: Change + rootCandidate: Change, ): ValueAnimator { val progressUpdater = ValueAnimator.ofFloat(0f, 1f) - val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet) + val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.deviceProperties.isTablet) progressUpdater.setDuration(timings.getDuration().toLong()) progressUpdater.interpolator = Interpolators.LINEAR @@ -868,7 +908,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.LINEAR, valueAnimator.animatedFraction, timings.appRevealStartOffset, - timings.appRevealEndOffset + timings.appRevealEndOffset, ) // Set the alpha of the shell layer (2 apps + divider) @@ -881,42 +921,44 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC var mDx = FloatProp( floatingView.startingPosition.left, - dp.widthPx / 2f - floatingView.startingPosition.width() / 2f, + dp.deviceProperties.widthPx / 2f - + floatingView.startingPosition.width() / 2f, Interpolators.clampToProgress( timings.getStagedRectXInterpolator(), timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) var mDy = FloatProp( floatingView.startingPosition.top, - dp.heightPx / 2f - floatingView.startingPosition.height() / 2f, + dp.deviceProperties.heightPx / 2f - + floatingView.startingPosition.height() / 2f, Interpolators.clampToProgress( Interpolators.EMPHASIZED, timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) var mScaleX = FloatProp( 1f /* start */, - dp.widthPx / floatingView.startingPosition.width(), + dp.deviceProperties.widthPx / floatingView.startingPosition.width(), Interpolators.clampToProgress( Interpolators.EMPHASIZED, timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) var mScaleY = FloatProp( 1f /* start */, - dp.heightPx / floatingView.startingPosition.height(), + dp.deviceProperties.heightPx / floatingView.startingPosition.height(), Interpolators.clampToProgress( Interpolators.EMPHASIZED, timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) + timings.stagedRectSlideEndOffset, + ), ) override fun onUpdate(percent: Float, initOnly: Boolean) { @@ -951,42 +993,25 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC transitionInfo: TransitionInfo, t: Transaction, finishCallback: Runnable, - windowingMode: Int + windowingMode: Int, ) { val launchAnimation = AnimatorSet() val progressUpdater = ValueAnimator.ofFloat(0f, 1f) progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION) progressUpdater.interpolator = Interpolators.EMPHASIZED - var rootCandidate: Change? = null - - for (change in transitionInfo.changes) { - val taskInfo: RunningTaskInfo = change.taskInfo ?: continue - - // TODO (b/316490565): Replace this logic when SplitBounds is available to - // startAnimation() and we can know the precise taskIds of launching tasks. - if ( - taskInfo.windowingMode == windowingMode && - (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) - ) { - // Found one! - rootCandidate = change - break - } - } - - // If we could not find a proper root candidate, something went wrong. - check(rootCandidate != null) { "Could not find a split root candidate" } - - // Recurse up the tree until parent is null, then we've found our root. - var parentToken: WindowContainerToken? = rootCandidate.parent - while (parentToken != null) { - rootCandidate = transitionInfo.getChange(parentToken) ?: break - parentToken = rootCandidate.parent - } - - // Make sure nothing weird happened, like getChange() returning null. - check(rootCandidate != null) { "Failed to find a root leash" } + val splitTree: Pair>? = extractTopParentAndChildren(transitionInfo) + check(splitTree != null) { "Could not find a split root candidate" } + val rootCandidate = splitTree.first + val stageRootTaskIds: Set = splitTree.second.map { it.taskInfo!!.taskId }.toSet() + val leafTasks: List = + transitionInfo.changes + .filter { + (TransitionUtil.isOpeningMode(it.mode) || it.mode == TRANSIT_CHANGE) && + it.taskInfo != null && + it.taskInfo!!.parentTaskId in stageRootTaskIds + } + .toList() // Starting position is a 34% size tile centered in the middle of the screen. // Ending position is the full device screen. @@ -994,10 +1019,10 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC val startingScale = 0.34f val startX = screenBounds.left + - ((screenBounds.right - screenBounds.left) * ((1 - startingScale) / 2f)) + ((screenBounds.right - screenBounds.left) * ((1 - startingScale) / 2f)) val startY = screenBounds.top + - ((screenBounds.bottom - screenBounds.top) * ((1 - startingScale) / 2f)) + ((screenBounds.bottom - screenBounds.top) * ((1 - startingScale) / 2f)) val endX = screenBounds.left val endY = screenBounds.top @@ -1020,6 +1045,42 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC override fun onAnimationEnd(animation: Animator) { finishCallback.run() } + + override fun onAnimationStart(animation: Animator) { + // Reset leaf and stage root tasks, animation can begin from freeform windows + for (leaf in leafTasks) { + val endAbsBounds = leaf.endAbsBounds + + t.setAlpha(leaf.leash, 1f) + t.setCrop( + leaf.leash, + 0f, + 0f, + endAbsBounds.width().toFloat(), + endAbsBounds.height().toFloat(), + ) + t.setPosition(leaf.leash, 0f, 0f) + } + + for (stageRoot in splitTree.second) { + val endAbsBounds = stageRoot.endAbsBounds + + t.setAlpha(stageRoot.leash, 1f) + t.setCrop( + stageRoot.leash, + 0f, + 0f, + endAbsBounds.width().toFloat(), + endAbsBounds.height().toFloat(), + ) + t.setPosition( + stageRoot.leash, + endAbsBounds.left.toFloat(), + endAbsBounds.top.toFloat(), + ) + } + t.apply() + } } ) @@ -1037,7 +1098,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC secondTaskId: Int, transitionInfo: TransitionInfo, t: Transaction, - finishCallback: Runnable + finishCallback: Runnable, + cornerRadius: Float, ) { var splitRoot1: Change? = null var splitRoot2: Change? = null @@ -1102,7 +1164,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC Interpolators.LINEAR, valueAnimator.animatedFraction, 0.8f, - 1f + 1f, ) for (leash in openingTargets) { animTransaction.setAlpha(leash, progress) @@ -1115,6 +1177,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC override fun onAnimationStart(animation: Animator) { for (leash in openingTargets) { animTransaction.show(leash).setAlpha(leash, 0.0f) + animTransaction.setCornerRadius(leash, cornerRadius) } animTransaction.apply() } @@ -1129,9 +1192,9 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC animator.start() } - private fun safeRemoveViewFromDragLayer(container: RecentsViewContainer, view: View?) { + private fun safeRemoveViewFromDragLayer(container: ActivityContext, view: View?) { if (view != null) { container.dragLayer.removeView(view) } } -} \ No newline at end of file +} diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java index b618546576..3a6d9b027b 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java @@ -17,6 +17,7 @@ package com.android.quickstep.util; import static com.android.app.animation.Interpolators.LINEAR; +import static com.android.app.animation.Interpolators.STANDARD; import android.view.animation.Interpolator; @@ -38,6 +39,8 @@ public interface SplitAnimationTimings { int TABLET_APP_PAIR_LAUNCH_DURATION = 998; /** Total duration (ms) for launching an app pair from its icon on phones. */ int PHONE_APP_PAIR_LAUNCH_DURATION = 915; + /** Total duration (ms) for fading out desktop tasks in split mode. */ + int DESKTOP_FADE_OUT_DURATION = 200; // Initialize timing classes so they can be accessed statically SplitAnimationTimings TABLET_OVERVIEW_TO_SPLIT = new TabletOverviewToSplitTimings(); @@ -83,6 +86,10 @@ public interface SplitAnimationTimings { return (float) getStagedRectSlideEnd() / getDuration(); } + default float getDesktopFadeSplitAnimationEndOffset() { + return (float) DESKTOP_FADE_OUT_DURATION / getDuration(); + } + // DEFAULT VALUES: We define default values here so that SplitAnimationTimings can be used // flexibly in animation-running functions, e.g. a single function that handles 2 types of split // animations. The values are not intended to be used, and can safely be removed if refactoring @@ -105,6 +112,10 @@ public interface SplitAnimationTimings { default Interpolator getGridSlidePrimaryInterpolator() { return LINEAR; } default Interpolator getGridSlideSecondaryInterpolator() { return LINEAR; } + default Interpolator getDesktopTaskFadeInterpolator() { + return LINEAR; + } + // Defaults for HomeToSplit default float getScrimFadeInStartOffset() { return 0; } default float getScrimFadeInEndOffset() { return 0; } @@ -120,5 +131,9 @@ public interface SplitAnimationTimings { default float getAppRevealEndOffset() { return 0; } default Interpolator getCellSplitInterpolator() { return LINEAR; } default Interpolator getIconFadeInterpolator() { return LINEAR; } + + default Interpolator getDesktopTaskScaleInterpolator() { + return STANDARD; + } } diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt index 38bbe601b6..2eb848216d 100644 --- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt +++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt @@ -16,44 +16,70 @@ package com.android.quickstep.util +import android.util.Log +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.TransitionInfo.FLAG_FIRST_CUSTOM import com.android.launcher3.util.SplitConfigurationOptions -import com.android.wm.shell.util.SplitBounds +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.shared.split.SplitBounds class SplitScreenUtils { companion object { - // TODO(b/254378592): Remove these methods when the two classes are reunited - /** Converts the shell version of SplitBounds to the launcher version */ - @JvmStatic - fun convertShellSplitBoundsToLauncher( - shellSplitBounds: SplitBounds? - ): SplitConfigurationOptions.SplitBounds? { - return if (shellSplitBounds == null) { - null + private const val TAG = "SplitScreenUtils" + + /** + * Given a TransitionInfo, generates the tree structure for those changes and extracts out + * the top most root and it's two immediate children. Changes can be provided in any order. + * + * @return a [Pair] where first -> top most split root, second -> [List] of 2, + * leftTop/bottomRight stage roots + */ + fun extractTopParentAndChildren( + transitionInfo: TransitionInfo + ): Pair>? { + val parentToChildren = mutableMapOf>() + val hasParent = mutableSetOf() + val taskChanges: List = getNonClosingChanges(transitionInfo) + + // 1. Build Parent-Child Relationships + for (change in taskChanges) { + // TODO (b/316490565): Replace this logic when SplitBounds is available to + // startAnimation() and we can know the precise taskIds of launching tasks. + change.parent?.let { parent -> + parentToChildren + .getOrPut(transitionInfo.getChange(parent)!!) { mutableListOf() } + .add(change) + hasParent.add(change) + } + } + + // 2. Find Top Parent + val topParent = taskChanges.firstOrNull { it !in hasParent } + + // 3. Extract Immediate Children + return if (topParent != null) { + val immediateChildren = parentToChildren.getOrDefault(topParent, emptyList()) + if (immediateChildren.size != 2) { + throw IllegalStateException("incorrect split stage root size") + } + Pair(topParent, immediateChildren) } else { - SplitConfigurationOptions.SplitBounds( - shellSplitBounds.leftTopBounds, shellSplitBounds.rightBottomBounds, - shellSplitBounds.leftTopTaskId, shellSplitBounds.rightBottomTaskId, - shellSplitBounds.snapPosition - ) + Log.w(TAG, "No top parent found") + null } } - /** Converts the launcher version of SplitBounds to the shell version */ - @JvmStatic - fun convertLauncherSplitBoundsToShell( - launcherSplitBounds: SplitConfigurationOptions.SplitBounds? - ): SplitBounds? { - return if (launcherSplitBounds == null) { - null - } else { - SplitBounds( - launcherSplitBounds.leftTopBounds, - launcherSplitBounds.rightBottomBounds, - launcherSplitBounds.leftTopTaskId, - launcherSplitBounds.rightBottomTaskId, - launcherSplitBounds.snapPosition - ) - } + + /** @return includes only opening + [TRANSIT_CHANGE] changes and the divider */ + private fun getNonClosingChanges(transitionInfo: TransitionInfo): List { + return transitionInfo.changes + .filter { change -> + (TransitionUtil.isOpeningMode(change.mode) || change.mode == TRANSIT_CHANGE) + && change.flags < FLAG_FIRST_CUSTOM + } + .toList() } } } diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index bcfcd8eb26..fd30e2037c 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -16,7 +16,6 @@ package com.android.quickstep.util; -import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP; @@ -35,8 +34,8 @@ import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_TASK import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK; -import static com.android.wm.shell.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -53,20 +52,18 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; -import android.os.SystemClock; import android.os.UserHandle; import android.util.Log; import android.util.Pair; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.view.View; import android.window.IRemoteTransitionFinishedCallback; import android.window.RemoteTransition; import android.window.RemoteTransitionStub; import android.window.TransitionInfo; +import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -75,12 +72,12 @@ import com.android.internal.logging.InstanceId; import com.android.launcher3.R; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.apppairs.AppPairIcon; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.IconProvider; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.uioverrides.QuickstepLauncher; @@ -94,23 +91,22 @@ import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RecentsModel; import com.android.quickstep.SplitSelectionListener; import com.android.quickstep.SystemUiProxy; -import com.android.quickstep.TaskAnimationManager; import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; import com.android.quickstep.views.SplitInstructionsView; -import com.android.systemui.animation.RemoteAnimationRunnerCompat; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.systemui.shared.system.QuickStepContract; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.splitscreen.ISplitSelectListener; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; /** @@ -121,7 +117,6 @@ public class SplitSelectStateController { private static final String TAG = "SplitSelectStateCtor"; private RecentsViewContainer mContainer; - private final Handler mHandler; private final RecentsModel mRecentTasksModel; @Nullable private Runnable mActivityBackCallback; @@ -154,9 +149,10 @@ public class SplitSelectStateController { /** * Should be a constant from {@link com.android.internal.jank.Cuj} or -1, does not need to be - * set for all launches. + * set for all launches. Used in conjunction with {@link #mLaunchingViewCuj} below. */ private int mLaunchCuj = -1; + private View mLaunchingViewCuj; private FloatingTaskView mFirstFloatingTaskView; private SplitInstructionsView mSplitInstructionsView; @@ -168,10 +164,12 @@ public class SplitSelectStateController { */ private Pair mSessionInstanceIds; + private boolean mIsDestroyed = false; + private final BackPressHandler mSplitBackHandler = new BackPressHandler() { @Override public boolean canHandleBack() { - return FeatureFlags.enableSplitContextually() && isSplitSelectActive(); + return isSplitSelectActive(); } @Override @@ -186,12 +184,11 @@ public class SplitSelectStateController { } }; - public SplitSelectStateController(RecentsViewContainer container, Handler handler, - StateManager stateManager, DepthController depthController, - StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel, - Runnable activityBackCallback) { + public SplitSelectStateController(RecentsViewContainer container, + StateManager stateManager, DepthController depthController, + StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel, + Runnable activityBackCallback) { mContainer = container; - mHandler = handler; mStatsLogManager = statsLogManager; mSystemUiProxy = systemUiProxy; mStateManager = stateManager; @@ -199,12 +196,13 @@ public class SplitSelectStateController { mRecentTasksModel = recentsModel; mActivityBackCallback = activityBackCallback; mSplitAnimationController = new SplitAnimationController(this); - mAppPairsController = new AppPairsController(mContainer.asContext(), this, statsLogManager); + mAppPairsController = new AppPairsController(mContainer, this, statsLogManager); mSplitSelectDataHolder = new SplitSelectDataHolder(mContainer.asContext()); } public void onDestroy() { mContainer = null; + mIsDestroyed = true; mActivityBackCallback = null; mAppPairsController.onDestroy(); mSplitSelectDataHolder.onDestroy(); @@ -219,8 +217,8 @@ public class SplitSelectStateController { * @param intent will be ignored if @param alreadyRunningTask is set */ public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition, - @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, - int alreadyRunningTask) { + @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, + int alreadyRunningTask) { mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent, alreadyRunningTask); createAndLogInstanceIdsForSession(); @@ -231,8 +229,8 @@ public class SplitSelectStateController { * running app. */ public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info, - @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, - StatsLogManager.EventEnum splitEvent) { + @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, + StatsLogManager.EventEnum splitEvent) { mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent); createAndLogInstanceIdsForSession(); } @@ -247,7 +245,7 @@ public class SplitSelectStateController { * tasks (i.e. searching for a running pair of tasks.) */ public void findLastActiveTasksAndRunCallback(@Nullable List componentKeys, - boolean findExactPairMatch, Consumer callback) { + boolean findExactPairMatch, Consumer callback) { mRecentTasksModel.getTasks(taskGroups -> { if (componentKeys == null || componentKeys.isEmpty()) { callback.accept(new Task[]{}); @@ -262,7 +260,7 @@ public class SplitSelectStateController { GroupTask groupTask = taskGroups.get(i); if (isInstanceOfAppPair( groupTask, componentKeys.get(0), componentKeys.get(1))) { - lastActiveTasks[0] = groupTask.task1; + lastActiveTasks[0] = ((SplitTask) groupTask).getTopLeftTask(); break; } } @@ -275,17 +273,15 @@ public class SplitSelectStateController { // Loop through tasks in reverse, since they are ordered with recent tasks last for (int j = taskGroups.size() - 1; j >= 0; j--) { GroupTask groupTask = taskGroups.get(j); - Task task1 = groupTask.task1; - // Don't add duplicate Tasks - if (isInstanceOfComponent(task1, key) - && !Arrays.asList(lastActiveTasks).contains(task1)) { - lastActiveTask = task1; - break; + // Account for desktop cases where there can be N tasks in the group + for (Task task : groupTask.getTasks()) { + if (isInstanceOfComponent(task, key) + && !Arrays.asList(lastActiveTasks).contains(task)) { + lastActiveTask = task; + break; + } } - Task task2 = groupTask.task2; - if (isInstanceOfComponent(task2, key) - && !Arrays.asList(lastActiveTasks).contains(task2)) { - lastActiveTask = task2; + if (lastActiveTask != null) { break; } } @@ -307,6 +303,10 @@ public class SplitSelectStateController { if (task == null || task.key.id == mSplitSelectDataHolder.getInitialTaskId()) { return false; } + if (task.key.baseIntent.getComponent() == null) { + Log.w(TAG, "Task has null component."); + return false; + } return task.key.baseIntent.getComponent().equals(componentKey.componentName) && task.key.userId == componentKey.user.getIdentifier(); @@ -317,12 +317,16 @@ public class SplitSelectStateController { * both permutations because task order is not guaranteed in GroupTasks. */ public boolean isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1, - @NonNull ComponentKey componentKey2) { - return ((isInstanceOfComponent(groupTask.task1, componentKey1) - && isInstanceOfComponent(groupTask.task2, componentKey2)) - || - (isInstanceOfComponent(groupTask.task1, componentKey2) - && isInstanceOfComponent(groupTask.task2, componentKey1))); + @NonNull ComponentKey componentKey2) { + if (groupTask instanceof SplitTask splitTask) { + return ((isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey1) + && isInstanceOfComponent(splitTask.getBottomRightTask(), componentKey2)) + || + (isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey2) + && isInstanceOfComponent(splitTask.getBottomRightTask(), + componentKey1))); + } + return false; } /** @@ -351,7 +355,7 @@ public class SplitSelectStateController { * animations are complete. */ public void launchSplitTasks(@PersistentSnapPosition int snapPosition, - @Nullable Consumer callback) { + @Nullable Consumer callback) { launchTasks(callback, false /* freezeTaskList */, snapPosition, mSessionInstanceIds.first); mStatsLogManager.logger() @@ -372,7 +376,7 @@ public class SplitSelectStateController { * A version of {@link #launchSplitTasks(int, Consumer)} that launches with default split ratio. */ public void launchSplitTasks(@Nullable Consumer callback) { - launchSplitTasks(SNAP_TO_50_50, callback); + launchSplitTasks(SNAP_TO_2_50_50, callback); } /** @@ -380,7 +384,7 @@ public class SplitSelectStateController { * ratio and no callback. */ public void launchSplitTasks() { - launchSplitTasks(SNAP_TO_50_50, null); + launchSplitTasks(SNAP_TO_2_50_50, null); } /** @@ -436,7 +440,7 @@ public class SplitSelectStateController { * foreground (quickswitch, launching previous pairs from overview) */ public void launchTasks(@Nullable Consumer callback, boolean freezeTaskList, - @PersistentSnapPosition int snapPosition, @Nullable InstanceId shellInstanceId) { + @PersistentSnapPosition int snapPosition, @Nullable InstanceId shellInstanceId) { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "launchSplitTasks"); final ActivityOptions options1 = ActivityOptions.makeBasic(); @@ -459,77 +463,41 @@ public class SplitSelectStateController { Bundle optionsBundle = options1.toBundle(); Bundle extrasBundle = new Bundle(1); extrasBundle.putParcelable(KEY_EXTRA_WIDGET_INTENT, widgetIntent); - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId, - secondTaskId, callback, "LaunchSplitPair"); - switch (launchData.getSplitLaunchType()) { - case SPLIT_TASK_TASK -> - mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, - null /* options2 */, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + final RemoteTransition remoteTransition = getRemoteTransition(firstTaskId, + secondTaskId, callback, "LaunchSplitPair"); + switch (launchData.getSplitLaunchType()) { + case SPLIT_TASK_TASK -> + mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, + null /* options2 */, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_TASK_PENDINGINTENT -> - mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle, - firstTaskId, extrasBundle, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + case SPLIT_TASK_PENDINGINTENT -> + mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle, + firstTaskId, extrasBundle, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_TASK_SHORTCUT -> - mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle, - firstTaskId, null /*options2*/, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + case SPLIT_TASK_SHORTCUT -> + mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle, + firstTaskId, null /*options2*/, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_PENDINGINTENT_TASK -> - mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle, - secondTaskId, null /*options2*/, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); + case SPLIT_PENDINGINTENT_TASK -> + mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle, + secondTaskId, null /*options2*/, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); - case SPLIT_PENDINGINTENT_PENDINGINTENT -> - mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut, - optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle, - initialStagePosition, snapPosition, remoteTransition, - shellInstanceId); + case SPLIT_PENDINGINTENT_PENDINGINTENT -> + mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut, + optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle, + initialStagePosition, snapPosition, remoteTransition, + shellInstanceId); - case SPLIT_SHORTCUT_TASK -> - mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle, - secondTaskId, null /*options2*/, initialStagePosition, snapPosition, - remoteTransition, shellInstanceId); - } - } else { - final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, secondTaskId, - callback); - switch (launchData.getSplitLaunchType()) { - case SPLIT_TASK_TASK -> - mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, - secondTaskId, null /* options2 */, initialStagePosition, - snapPosition, adapter, shellInstanceId); - - case SPLIT_TASK_PENDINGINTENT -> - mSystemUiProxy.startIntentAndTaskWithLegacyTransition(secondPI, - secondUserId, optionsBundle, firstTaskId, null /*options2*/, - initialStagePosition, snapPosition, adapter, shellInstanceId); - - case SPLIT_TASK_SHORTCUT -> - mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(secondShortcut, - optionsBundle, firstTaskId, null /*options2*/, initialStagePosition, - snapPosition, adapter, shellInstanceId); - - case SPLIT_PENDINGINTENT_TASK -> - mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId, - optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, snapPosition, adapter, shellInstanceId); - - case SPLIT_PENDINGINTENT_PENDINGINTENT -> - mSystemUiProxy.startIntentsWithLegacyTransition(firstPI, firstUserId, - firstShortcut, optionsBundle, secondPI, secondUserId, - secondShortcut, null /*options2*/, initialStagePosition, - snapPosition, adapter, shellInstanceId); - - case SPLIT_SHORTCUT_TASK -> - mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(firstShortcut, - optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, snapPosition, adapter, shellInstanceId); - } + case SPLIT_SHORTCUT_TASK -> + mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle, + secondTaskId, null /*options2*/, initialStagePosition, snapPosition, + remoteTransition, shellInstanceId); } + } /** @@ -540,9 +508,9 @@ public class SplitSelectStateController { * GroupedTaskView, int, int, int, Consumer, boolean, int, RemoteTransition)} */ public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView, - int firstTaskId, int secondTaskId, @StagePosition int stagePosition, - Consumer callback, boolean freezeTaskList, - @PersistentSnapPosition int snapPosition) { + int firstTaskId, int secondTaskId, @StagePosition int stagePosition, + Consumer callback, boolean freezeTaskList, + @PersistentSnapPosition int snapPosition) { launchExistingSplitPair( groupedTaskView, firstTaskId, @@ -565,9 +533,9 @@ public class SplitSelectStateController { * NOTE: This is not to be used to launch AppPairs. */ public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView, - int firstTaskId, int secondTaskId, @StagePosition int stagePosition, - Consumer callback, boolean freezeTaskList, - @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition) { + int firstTaskId, int secondTaskId, @StagePosition int stagePosition, + Consumer callback, boolean freezeTaskList, + @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition) { mLaunchingTaskView = groupedTaskView; final ActivityOptions options1 = ActivityOptions.makeBasic(); if (freezeTaskList) { @@ -575,20 +543,13 @@ public class SplitSelectStateController { } Bundle optionsBundle = options1.toBundle(); - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - final RemoteTransition transition = remoteTransition == null - ? getShellRemoteTransition( - firstTaskId, secondTaskId, callback, "LaunchExistingPair") - : remoteTransition; - mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */, - stagePosition, snapPosition, transition, null /*shellInstanceId*/); - } else { - final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, - secondTaskId, callback); - mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, secondTaskId, - null /* options2 */, stagePosition, snapPosition, adapter, - null /*shellInstanceId*/); - } + final RemoteTransition transition = remoteTransition == null + ? getRemoteTransition( + firstTaskId, secondTaskId, callback, "LaunchExistingPair") + : remoteTransition; + mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */, + stagePosition, snapPosition, transition, null /*shellInstanceId*/); + } /** @@ -614,44 +575,24 @@ public class SplitSelectStateController { ActivityThread.currentActivityThread().getApplicationThread(), "LaunchAppFullscreen"); InstanceId instanceId = mSessionInstanceIds.first; - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - switch (launchData.getSplitLaunchType()) { - case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId, - optionsBundle, secondTaskId, null /* options2 */, initialStagePosition, - SNAP_TO_50_50, remoteTransition, instanceId); - case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI, - firstUserId, optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId); - case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask( - initialShortcut, optionsBundle, firstTaskId, null /* options2 */, - initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId); - } - } else { - final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, - secondTaskId, callback); - switch (launchData.getSplitLaunchType()) { - case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasksWithLegacyTransition( - firstTaskId, optionsBundle, secondTaskId, null /* options2 */, - initialStagePosition, SNAP_TO_50_50, adapter, instanceId); - case SPLIT_SINGLE_INTENT_FULLSCREEN -> - mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId, - optionsBundle, secondTaskId, null /*options2*/, - initialStagePosition, SNAP_TO_50_50, adapter, instanceId); - case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> - mSystemUiProxy.startShortcutAndTaskWithLegacyTransition( - initialShortcut, optionsBundle, firstTaskId, null /* options2 */, - initialStagePosition, SNAP_TO_50_50, adapter, instanceId); - } + switch (launchData.getSplitLaunchType()) { + case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId, + optionsBundle, secondTaskId, null /* options2 */, initialStagePosition, + SNAP_TO_2_50_50, remoteTransition, instanceId); + case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI, + firstUserId, optionsBundle, secondTaskId, null /*options2*/, + initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId); + case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask( + initialShortcut, optionsBundle, firstTaskId, null /* options2 */, + initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId); } } /** * Init {@code SplitFromDesktopController} */ - public void initSplitFromDesktopController(QuickstepLauncher launcher, - OverviewComponentObserver overviewComponentObserver) { - initSplitFromDesktopController( - new SplitFromDesktopController(launcher, overviewComponentObserver)); + public void initSplitFromDesktopController(QuickstepLauncher launcher) { + initSplitFromDesktopController(new SplitFromDesktopController(launcher)); } @VisibleForTesting @@ -659,22 +600,14 @@ public class SplitSelectStateController { mSplitFromDesktopController = controller; } - private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId, - @Nullable Consumer callback, String transitionName) { + private RemoteTransition getRemoteTransition(int firstTaskId, int secondTaskId, + @Nullable Consumer callback, String transitionName) { final RemoteSplitLaunchTransitionRunner animationRunner = new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); return new RemoteTransition(animationRunner, ActivityThread.currentActivityThread().getApplicationThread(), transitionName); } - private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId, - @Nullable Consumer callback) { - final RemoteSplitLaunchAnimationRunner animationRunner = - new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback); - return new RemoteAnimationAdapter(animationRunner, 300, 150, - ActivityThread.currentActivityThread().getApplicationThread()); - } - /** * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from * {@link #mSplitSelectDataHolder} @@ -727,7 +660,12 @@ public class SplitSelectStateController { return mSplitAnimationController; } - public void setLaunchingCuj(int launchCuj) { + /** + * Set params to invoke a trace session for the given view and CUJ when we begin animating the + * split launch AFTER we get a response from Shell. + */ + public void setLaunchingCuj(View launchingView, int launchCuj) { + mLaunchingViewCuj = launchingView; mLaunchCuj = launchCuj; } @@ -741,16 +679,16 @@ public class SplitSelectStateController { private Consumer mFinishCallback; RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, - @Nullable Consumer callback) { + @Nullable Consumer callback) { mInitialTaskId = initialTaskId; mSecondTaskId = secondTaskId; mFinishCallback = callback; } @Override - public void startAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, - IRemoteTransitionFinishedCallback finishedCallback) { + public void startAnimation(IBinder transition, TransitionInfo transitionInfo, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishedCallback) { final Runnable finishAdapter = () -> { try { finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); @@ -765,6 +703,9 @@ public class SplitSelectStateController { && mLaunchingTaskView.getRecentsView() != null && mLaunchingTaskView.getRecentsView().isTaskViewVisible( mLaunchingTaskView); + if (mLaunchingViewCuj != null && mLaunchCuj != -1) { + InteractionJankMonitorWrapper.begin(mLaunchingViewCuj, mLaunchCuj); + } mSplitAnimationController.playSplitLaunchAnimation( shouldLaunchFromTaskView ? mLaunchingTaskView : null, mLaunchingIconView, @@ -775,10 +716,11 @@ public class SplitSelectStateController { null /* nonApps */, mStateManager, mDepthController, - info, t, () -> { + transitionInfo, t, () -> { finishAdapter.run(); cleanup(true /*success*/); - }); + }, + QuickStepContract.getWindowCornerRadius(mContainer.asContext())); }); } @@ -805,60 +747,15 @@ public class SplitSelectStateController { } /** - * LEGACY - * Remote animation runner for animation to launch an app. - */ - private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat { - - private final int mInitialTaskId; - private final int mSecondTaskId; - private final Consumer mSuccessCallback; - - RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId, - @Nullable Consumer successCallback) { - mInitialTaskId = initialTaskId; - mSecondTaskId = secondTaskId; - mSuccessCallback = successCallback; - } - - @Override - public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - Runnable finishedCallback) { - postAsyncCallback(mHandler, - () -> mSplitAnimationController.playSplitLaunchAnimation(mLaunchingTaskView, - mLaunchingIconView, mInitialTaskId, mSecondTaskId, apps, wallpapers, - nonApps, mStateManager, mDepthController, null /* info */, null /* t */, - () -> { - finishedCallback.run(); - if (mSuccessCallback != null) { - mSuccessCallback.accept(true); - } - resetState(); - })); - } - - @Override - public void onAnimationCancelled() { - postAsyncCallback(mHandler, () -> { - if (mSuccessCallback != null) { - // Launching legacy tasks while recents animation is running will always cause - // onAnimationCancelled to be called (should be fixed w/ shell transitions?) - mSuccessCallback.accept(mRecentsAnimationRunning); - } - resetState(); - }); - } - } - - /** - * To be called whenever we exit split selection state. If - * {@link FeatureFlags#enableSplitContextually()} is set, this should be the + * To be called whenever we exit split selection state. This should be the * central way split is getting reset, which should then go through the callbacks to reset * other state. */ public void resetState() { mSplitSelectDataHolder.resetState(); + if (!mIsDestroyed) { + mContainer.getOverviewPanel().resetDesktopTaskFromSplitSelectState(); + } dispatchOnSplitSelectionExit(); mRecentsAnimationRunning = false; mLaunchingTaskView = null; @@ -873,6 +770,7 @@ public class SplitSelectStateController { InteractionJankMonitorWrapper.end(mLaunchCuj); } mLaunchCuj = -1; + mLaunchingViewCuj = null; if (mSessionInstanceIds != null) { mStatsLogManager.logger() @@ -957,32 +855,24 @@ public class SplitSelectStateController { private final int mSplitPlaceholderSize; private final int mSplitPlaceholderInset; private ActivityManager.RunningTaskInfo mTaskInfo; - private ISplitSelectListener mSplitSelectListener; + private DesktopSplitSelectListenerImpl mSplitSelectListener; private Drawable mAppIcon; - public SplitFromDesktopController(QuickstepLauncher launcher, - OverviewComponentObserver overviewComponentObserver) { + public SplitFromDesktopController(QuickstepLauncher launcher) { mLauncher = launcher; - mOverviewComponentObserver = overviewComponentObserver; + mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(launcher); mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize( R.dimen.split_placeholder_size); mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize( R.dimen.split_placeholder_inset); - mSplitSelectListener = new ISplitSelectListener.Stub() { - @Override - public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, - int splitPosition, Rect taskBounds) { - MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition, - taskBounds)); - return true; - } - }; + mSplitSelectListener = new DesktopSplitSelectListenerImpl(this); SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener); } void onDestroy() { SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener( mSplitSelectListener); + mSplitSelectListener.release(); mSplitSelectListener = null; } @@ -993,67 +883,93 @@ public class SplitSelectStateController { * @param taskBounds the bounds of the task, used for {@link FloatingTaskView} animation */ public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, - int splitPosition, Rect taskBounds) { + int splitPosition, Rect taskBounds, boolean startRecents, + @Nullable WindowContainerTransaction withRecentsWct) { mTaskInfo = taskInfo; String packageName = mTaskInfo.realActivity.getPackageName(); PackageManager pm = mLauncher.getApplicationContext().getPackageManager(); IconProvider provider = new IconProvider(mLauncher.getApplicationContext()); try { mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, - PackageManager.ComponentInfoFlags.of(0))); + PackageManager.ComponentInfoFlags.of(0))); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Package not found: " + packageName, e); } - RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks( - SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()), - false /* allowMinimizeSplitScreen */); - DesktopSplitRecentsAnimationListener listener = - new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds); - - MAIN_EXECUTOR.execute(() -> { - callbacks.addListener(listener); - UI_HELPER_EXECUTOR.execute( - // Transition from app to enter stage split in launcher with - // recents animation. - () -> ActivityManagerWrapper.getInstance().startRecentsActivity( - mOverviewComponentObserver.getOverviewIntent(), - SystemClock.uptimeMillis(), callbacks, null, null)); - }); + final DesktopSplitRecentsAnimation animation = new DesktopSplitRecentsAnimation( + splitPosition, taskBounds); + final Runnable updateTaskbarRunnable = () -> { + final LauncherTaskbarUIController c = mLauncher.getTaskbarUIController(); + if (c != null) { + c.updateTaskbarLauncherStateGoingHome(); + } + }; + if (startRecents) { + RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(mContainer); + callbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() { + @Override + public void onRecentsAnimationStart(RecentsAnimationController controller, + RecentsAnimationTargets targets, + @Nullable TransitionInfo transitionInfo) { + animation.start(() -> { + controller.finish( + true /* toRecents */, + updateTaskbarRunnable, + false /* sendUserLeaveHint */); + }); + } + }); + UI_HELPER_EXECUTOR.execute(() -> { + // Transition from app to enter stage split in launcher with recents animation + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + options.setTransientLaunch(); + SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) + .startRecentsActivity( + mOverviewComponentObserver.getOverviewIntent(), options, + callbacks, false /* useSyntheticRecentsTransition */, + withRecentsWct, + ExternalDisplaysKt.getSafeDisplayId(taskInfo)); + }); + } else { + animation.start(updateTaskbarRunnable); + } } - private class DesktopSplitRecentsAnimationListener implements - RecentsAnimationCallbacks.RecentsAnimationListener { + private class DesktopSplitRecentsAnimation { private final Rect mTempRect = new Rect(); private final RectF mTaskBounds = new RectF(); private final int mSplitPosition; - DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds) { + DesktopSplitRecentsAnimation(int splitPosition, Rect taskBounds) { mSplitPosition = splitPosition; mTaskBounds.set(taskBounds); } - @Override - public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { - StatsLogManager.LauncherEvent launcherDesktopSplitEvent = + void start(@NonNull Runnable onAnimationStart) { + final StatsLogManager.LauncherEvent launcherDesktopSplitEvent = mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ? LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM : LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP; setInitialTaskSelect(mTaskInfo, mSplitPosition, null, launcherDesktopSplitEvent); - RecentsView recentsView = mLauncher.getOverviewPanel(); + final RecentsView recentsView = mLauncher.getOverviewPanel(); recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds( mSplitPlaceholderSize, mSplitPlaceholderInset, mLauncher.getDeviceProfile(), getActiveSplitStagePosition(), mTempRect); - PendingAnimation anim = new PendingAnimation( + final PendingAnimation anim = new PendingAnimation( SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration()); final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView( mLauncher, mLauncher.getDragLayer(), null /* thumbnail */, mAppIcon, new RectF()); + floatingTaskView.setOnClickListener(view -> + getSplitAnimationController() + .playAnimPlaceholderToFullscreen(mContainer, view, + Optional.of(() -> resetState()))); floatingTaskView.setAlpha(1); floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); @@ -1062,17 +978,58 @@ public class SplitSelectStateController { anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - controller.finish(true /* toRecents */, null /* onFinishComplete */, - false /* sendUserLeaveHint */); + onAnimationStart.run(); } @Override public void onAnimationEnd(Animator animation) { SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) .onDesktopSplitSelectAnimComplete(mTaskInfo); } + @Override + public void onAnimationCancel(Animator animation) { + mLauncher.getDragLayer().removeView(floatingTaskView); + getSplitAnimationController() + .removeSplitInstructionsView(mLauncher); + resetState(); + } }); + anim.add(getSplitAnimationController() + .getShowSplitInstructionsAnim(mLauncher).buildAnim()); anim.buildAnim().start(); } } } -} \ No newline at end of file + + /** + * Wrapper for the ISplitSelectListener stub to prevent lingering references to the launcher + * activity via the controller. + */ + private static class DesktopSplitSelectListenerImpl extends ISplitSelectListener.Stub { + + private SplitFromDesktopController mController; + + DesktopSplitSelectListenerImpl(@NonNull SplitFromDesktopController controller) { + mController = controller; + } + + /** + * Clears any references to the controller. + */ + void release() { + mController = null; + } + + @Override + public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, + int splitPosition, Rect taskBounds, boolean startRecents, + @Nullable WindowContainerTransaction withRecentsWct) { + MAIN_EXECUTOR.execute(() -> { + if (mController != null) { + mController.enterSplitSelect(taskInfo, splitPosition, taskBounds, + startRecents, withRecentsWct); + } + }); + return true; + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java index 4962367bd2..abd24588d4 100644 --- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java +++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; +import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import android.animation.Animator; @@ -48,8 +49,11 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; +import java.util.Collections; + /** Handles when the stage split lands on the home screen. */ public class SplitToWorkspaceController { @@ -86,7 +90,7 @@ public class SplitToWorkspaceController { MODEL_EXECUTOR.execute(() -> { PackageItemInfo infoInOut = new PackageItemInfo(pendingIntent.getCreatorPackage(), pendingIntent.getCreatorUserHandle()); - mIconCache.getTitleAndIconForApp(infoInOut, false); + mIconCache.getTitleAndIconForApp(infoInOut, DEFAULT_LOOKUP_FLAG); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); view.post(() -> { @@ -133,17 +137,27 @@ public class SplitToWorkspaceController { // Use Launcher's default click handler return false; } - - mController.setSecondTask(intent, user, (ItemInfo) tag); - - startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher)); + // Check for background task matching this tag; if we find one, set second task + // via task instead of intent so the bounds and windowing mode will be corrected. + mController.findLastActiveTasksAndRunCallback( + Collections.singletonList(((ItemInfo) tag).getComponentKey()), + false /* findExactPairMatch */, + foundTasks -> { + Task foundTask = foundTasks[0]; + if (foundTask != null) { + mController.setSecondTask(foundTask, (ItemInfo) tag); + } else { + mController.setSecondTask(intent, user, (ItemInfo) tag); + } + startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher)); + }); return true; } private void startWorkspaceAnimation(@NonNull View view, @Nullable Bitmap bitmap, @Nullable Drawable icon) { DeviceProfile dp = mLauncher.getDeviceProfile(); - boolean isTablet = dp.isTablet; + boolean isTablet = dp.getDeviceProperties().isTablet(); SplitAnimationTimings timings = AnimUtils.getDeviceSplitToConfirmTimings(isTablet); PendingAnimation pendingAnimation = new PendingAnimation(timings.getDuration()); @@ -205,6 +219,6 @@ public class SplitToWorkspaceController { } private boolean shouldIgnoreSecondSplitLaunch() { - return !FeatureFlags.enableSplitContextually() || !mController.isSplitSelectActive(); + return !mController.isSplitSelectActive(); } } diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java index 5e42b9001b..e3fc56178e 100644 --- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java +++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java @@ -16,7 +16,6 @@ package com.android.quickstep.util; -import static com.android.launcher3.config.FeatureFlags.enableSplitContextually; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; @@ -27,9 +26,10 @@ import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITIO import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.graphics.Rect; import android.graphics.RectF; -import android.os.SystemClock; +import android.window.TransitionInfo; import androidx.annotation.BinderThread; @@ -37,10 +37,10 @@ import com.android.launcher3.R; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.taskbar.LauncherTaskbarUIController; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.quickstep.BaseContainerInterface; import com.android.quickstep.OverviewComponentObserver; import com.android.quickstep.RecentsAnimationCallbacks; import com.android.quickstep.RecentsAnimationController; -import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RecentsModel; import com.android.quickstep.SystemUiProxy; @@ -55,20 +55,16 @@ public class SplitWithKeyboardShortcutController { private final QuickstepLauncher mLauncher; private final SplitSelectStateController mController; - private final RecentsAnimationDeviceState mDeviceState; private final OverviewComponentObserver mOverviewComponentObserver; private final int mSplitPlaceholderSize; private final int mSplitPlaceholderInset; - public SplitWithKeyboardShortcutController(QuickstepLauncher launcher, - SplitSelectStateController controller, - OverviewComponentObserver overviewComponentObserver, - RecentsAnimationDeviceState deviceState) { + public SplitWithKeyboardShortcutController( + QuickstepLauncher launcher, SplitSelectStateController controller) { mLauncher = launcher; mController = controller; - mDeviceState = deviceState; - mOverviewComponentObserver = overviewComponentObserver; + mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(launcher); mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize( R.dimen.split_placeholder_size); @@ -77,50 +73,54 @@ public class SplitWithKeyboardShortcutController { } @BinderThread - public void enterStageSplit(boolean leftOrTop) { - if (!enableSplitContextually() || - // Do not enter stage split from keyboard shortcuts if the user is already in split - TopTaskTracker.INSTANCE.get(mLauncher).getRunningSplitTaskIds().length == 2) { + public void enterStageSplit(boolean leftOrTop, int displayId) { + if (TopTaskTracker.INSTANCE.get(mLauncher).getRunningSplitTaskIds().length == 2) { + // Do not enter stage split from keyboard shortcuts if the user is already in split + return; + } + BaseContainerInterface containerInterface = + mOverviewComponentObserver.getContainerInterface(displayId); + if (containerInterface == null) { return; } RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks( - SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()), - false /* allowMinimizeSplitScreen */); + containerInterface.getCreatedContainer()); SplitWithKeyboardShortcutRecentsAnimationListener listener = new SplitWithKeyboardShortcutRecentsAnimationListener(leftOrTop); MAIN_EXECUTOR.execute(() -> { callbacks.addListener(listener); - UI_HELPER_EXECUTOR.execute( - // Transition from fullscreen app to enter stage split in launcher with - // recents animation. - () -> ActivityManagerWrapper.getInstance().startRecentsActivity( - mOverviewComponentObserver.getOverviewIntent(), - SystemClock.uptimeMillis(), callbacks, null, null)); + UI_HELPER_EXECUTOR.execute(() -> { + // Transition from fullscreen app to enter stage split in launcher with + // recents animation + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + options.setTransientLaunch(); + SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) + .startRecentsActivity(mOverviewComponentObserver.getOverviewIntent(), + ActivityOptions.makeBasic(), callbacks, + false /* useSyntheticRecentsTransition */, null, displayId); + }); }); } - public void onDestroy() { - mOverviewComponentObserver.onDestroy(); - mDeviceState.destroy(); - } - private class SplitWithKeyboardShortcutRecentsAnimationListener implements RecentsAnimationCallbacks.RecentsAnimationListener { private final boolean mLeftOrTop; private final Rect mTempRect = new Rect(); + private final ActivityManager.RunningTaskInfo mRunningTaskInfo; private SplitWithKeyboardShortcutRecentsAnimationListener(boolean leftOrTop) { mLeftOrTop = leftOrTop; + mRunningTaskInfo = ActivityManagerWrapper.getInstance().getRunningTask(); } @Override public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { - ActivityManager.RunningTaskInfo runningTaskInfo = - ActivityManagerWrapper.getInstance().getRunningTask(); - mController.setInitialTaskSelect(runningTaskInfo, + RecentsAnimationTargets targets, TransitionInfo transitionInfo) { + mController.setInitialTaskSelect(mRunningTaskInfo, mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT, null /* itemInfo */, mLeftOrTop ? LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP @@ -136,14 +136,15 @@ public class SplitWithKeyboardShortcutController { RectF startingTaskRect = new RectF(); final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView( mLauncher, mLauncher.getDragLayer(), - controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(), + controller.screenshotTask(mRunningTaskInfo.taskId).getThumbnail(), null /* icon */, startingTaskRect); + Task task = Task.from(new Task.TaskKey(mRunningTaskInfo), mRunningTaskInfo, + false /* isLocked */); RecentsModel.INSTANCE.get(mLauncher.getApplicationContext()) .getIconCache() - .updateIconInBackground( - Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo, - false /* isLocked */), - (task) -> floatingTaskView.setIcon(task.icon)); + .getIconInBackground( + task, + (icon, contentDescription, title) -> floatingTaskView.setIcon(icon)); floatingTaskView.setAlpha(1); floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java index 997a842dc2..a2856a6012 100644 --- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java +++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java @@ -49,6 +49,7 @@ import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DynamicResource; import com.android.quickstep.views.RecentsView; import com.android.systemui.plugins.ResourceProvider; @@ -63,8 +64,7 @@ public class StaggeredWorkspaceAnim { private static final int APP_CLOSE_ROW_START_DELAY_MS = 10; // Should be used for animations running alongside this StaggeredWorkspaceAnim. public static final int DURATION_MS = 250; - public static final int DURATION_TASKBAR_MS = - QuickstepTransitionManager.getTaskbarToHomeDuration(); + private final int mTaskbarDurationInMs; private static final float MAX_VELOCITY_PX_PER_S = 22f; @@ -81,6 +81,10 @@ public class StaggeredWorkspaceAnim { public StaggeredWorkspaceAnim(QuickstepLauncher launcher, float velocity, boolean animateOverviewScrim, @Nullable View ignoredView, boolean staggerWorkspace) { + boolean isPinnedTaskbarAndNotInDesktopMode = DisplayController.isPinnedTaskbar(launcher) + && !DisplayController.isInDesktopMode(launcher); + mTaskbarDurationInMs = QuickstepTransitionManager.getTaskbarToHomeDuration( + isPinnedTaskbarAndNotInDesktopMode); prepareToAnimate(launcher, animateOverviewScrim); mIgnoredView = ignoredView; @@ -93,7 +97,7 @@ public class StaggeredWorkspaceAnim { .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y); DeviceProfile grid = launcher.getDeviceProfile(); - long duration = grid.isTaskbarPresent ? DURATION_TASKBAR_MS : DURATION_MS; + long duration = grid.isTaskbarPresent ? mTaskbarDurationInMs : DURATION_MS; if (staggerWorkspace) { Workspace workspace = launcher.getWorkspace(); Hotseat hotseat = launcher.getHotseat(); diff --git a/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java index 8006a97936..f29df91e28 100644 --- a/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java +++ b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java @@ -111,6 +111,15 @@ public class SurfaceTransaction { return this; } + /** + * @param radius The radius for the background blur to apply to the surface. + * @return this Builder + */ + public SurfaceProperties setBackgroundBlurRadius(int radius) { + mTransaction.setBackgroundBlurRadius(mSurface, radius); + return this; + } + /** * Requests to show the given surface. * @return this Builder @@ -119,6 +128,15 @@ public class SurfaceTransaction { mTransaction.show(mSurface); return this; } + + /** + * Requests to remove the given surface. + * @return this Builder + */ + public SurfaceProperties setRemove() { + mTransaction.remove(mSurface); + return this; + } } /** @@ -131,6 +149,7 @@ public class SurfaceTransaction { public Rect windowCrop = null; public float cornerRadius = 0; public float shadowRadius = 0; + public int backgroundBlurRadius = 0; protected MockProperties() { super(null); @@ -171,9 +190,20 @@ public class SurfaceTransaction { return this; } + @Override + public SurfaceProperties setBackgroundBlurRadius(int radius) { + this.backgroundBlurRadius = radius; + return this; + } + @Override public SurfaceProperties setShow() { return this; } + + @Override + public SurfaceProperties setRemove() { + return this; + } } } diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java index c3a7bfeac5..49c69e248c 100644 --- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java +++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java @@ -18,6 +18,7 @@ package com.android.quickstep.util; import android.animation.Animator; import android.animation.RectEvaluator; +import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; @@ -25,6 +26,7 @@ import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; +import android.util.Rational; import android.view.Surface; import android.view.SurfaceControl; import android.view.View; @@ -39,7 +41,7 @@ import com.android.launcher3.icons.IconProvider; import com.android.quickstep.TaskAnimationManager; import com.android.systemui.shared.pip.PipSurfaceTransactionHelper; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; -import com.android.wm.shell.pip.PipContentOverlay; +import com.android.wm.shell.shared.pip.PipContentOverlay; /** * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window @@ -50,8 +52,6 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { private static final float END_PROGRESS = 1.0f; - private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f; - private final int mTaskId; private final ActivityInfo mActivityInfo; private final SurfaceControl mLeash; @@ -137,8 +137,13 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { mDestinationBoundsTransformed.set(destinationBoundsTransformed); mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius, shadowRadius); - final float aspectRatio = destinationBounds.width() / (float) destinationBounds.height(); + final Rational aspectRatio = new Rational( + destinationBounds.width(), destinationBounds.height()); String reasonForCreateOverlay = null; // For debugging purpose. + + // Slightly larger app bounds to allow for off by 1 pixel source-rect-hint errors. + Rect overflowAppBounds = new Rect(appBounds.left - 1, appBounds.top - 1, + appBounds.right + 1, appBounds.bottom + 1); if (sourceRectHint.isEmpty()) { reasonForCreateOverlay = "Source rect hint is empty"; } else if (sourceRectHint.width() < destinationBounds.width() @@ -149,40 +154,27 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { // animation in this case. reasonForCreateOverlay = "Source rect hint is too small " + sourceRectHint; sourceRectHint.setEmpty(); - } else if (!appBounds.contains(sourceRectHint)) { + } else if (!overflowAppBounds.contains(sourceRectHint)) { // This is a situation in which the source hint rect is outside the app bounds, so it is // not a valid rectangle to use for cropping app surface reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint; sourceRectHint.setEmpty(); - } else if (Math.abs( - aspectRatio - (sourceRectHint.width() / (float) sourceRectHint.height())) - > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) { - // The source rect hint does not aspect ratio - reasonForCreateOverlay = "Source rect hint does not match aspect ratio " - + sourceRectHint + " aspect ratio " + aspectRatio; - sourceRectHint.setEmpty(); + } else { + final Rational srcAspectRatio = new Rational( + sourceRectHint.width(), sourceRectHint.height()); + if (!PictureInPictureParams.isSameAspectRatio(destinationBounds, srcAspectRatio)) { + // The aspect ratio of destination bounds does not match source rect hint. + // We use the aspect ratio of source rect hint to check against destination bounds + // here to avoid upscaling error. + reasonForCreateOverlay = "Source rect hint:" + sourceRectHint + + " does not match destination bounds:" + destinationBounds; + sourceRectHint.setEmpty(); + } } if (sourceRectHint.isEmpty()) { - // Crop a Rect matches the aspect ratio and pivots at the center point. - // To make the animation path simplified. - if ((appBounds.width() / (float) appBounds.height()) > aspectRatio) { - // use the full height. - mSourceRectHint.set(0, 0, - (int) (appBounds.height() * aspectRatio), appBounds.height()); - mSourceRectHint.offset( - (appBounds.width() - mSourceRectHint.width()) / 2, 0); - } else { - // use the full width. - mSourceRectHint.set(0, 0, - appBounds.width(), (int) (appBounds.width() / aspectRatio)); - mSourceRectHint.offset( - 0, (appBounds.height() - mSourceRectHint.height()) / 2); - } - - // Create a new overlay layer. We do not call detach on this instance, it's propagated - // to other classes like PipTaskOrganizer / RecentsAnimationController to complete - // the cleanup. + mSourceRectHint.set( + getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio.floatValue())); mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(), mAppBounds, mDestinationBounds, new IconProvider(context).getIcon(mActivityInfo), appIconSizePx); @@ -225,6 +217,26 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { addOnUpdateListener(this::onAnimationUpdate); } + /** + * Crop a Rect matches the aspect ratio and pivots at the center point. + */ + private Rect getEnterPipWithOverlaySrcRectHint(Rect appBounds, float aspectRatio) { + final float appBoundsAspectRatio = appBounds.width() / (float) appBounds.height(); + final int width, height; + int left = appBounds.left; + int top = appBounds.top; + if (appBoundsAspectRatio < aspectRatio) { + width = appBounds.width(); + height = (int) (width / aspectRatio); + top = appBounds.top + (appBounds.height() - height) / 2; + } else { + height = appBounds.height(); + width = (int) (height * aspectRatio); + left = appBounds.left + (appBounds.width() - width) / 2; + } + return new Rect(left, top, left + width, top + height); + } + private void onAnimationUpdate(RectF currentRect, float progress) { if (mHasAnimationEnded) return; final SurfaceControl.Transaction tx = @@ -441,13 +453,22 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { return this; } + public Builder setDisplayCutoutInsets(@NonNull Rect displayCutoutInsets) { + mDisplayCutoutInsets = new Rect(displayCutoutInsets); + return this; + } + public SwipePipToHomeAnimator build() { if (mDestinationBoundsTransformed.isEmpty()) { mDestinationBoundsTransformed.set(mDestinationBounds); } // adjust the mSourceRectHint / mAppBounds by display cutout if applicable. if (mSourceRectHint != null && mDisplayCutoutInsets != null) { - if (mFromRotation == Surface.ROTATION_90) { + if (mFromRotation == Surface.ROTATION_0) { + // TODO: this is to special case the issues on Foldable device + // with display cutout. + mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top); + } else if (mFromRotation == Surface.ROTATION_90) { mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top); } else if (mFromRotation == Surface.ROTATION_270) { mAppBounds.inset(mDisplayCutoutInsets); @@ -461,15 +482,6 @@ public class SwipePipToHomeAnimator extends RectFSpringAnim { } } - private static class RotatedPosition { - private final float degree; - private final float positionX; - private final float positionY; - - private RotatedPosition(float degree, float positionX, float positionY) { - this.degree = degree; - this.positionX = positionX; - this.positionY = positionY; - } + private record RotatedPosition(float degree, float positionX, float positionY) { } } diff --git a/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt b/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt index 5f4388c918..1ff05dae56 100644 --- a/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt +++ b/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt @@ -47,6 +47,22 @@ object SystemUiFlagUtils { !hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY) } + /** + * Taskbar is hidden whenever the device is dreaming. The dreaming state includes the + * interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in when + * the device is asleep, the second condition extends ensures that the transition from and to + * the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar hide/reveal + * animation timings. The Taskbar can show when dreaming if the glanceable hub is showing on + * top. + */ + @JvmStatic + fun isTaskbarHidden(@SystemUiStateFlags flags: Long): Boolean { + return ((hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_DEVICE_DREAMING) && + !hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING)) || + (flags and QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK) != + QuickStepContract.WAKEFULNESS_AWAKE) + } + private fun hasAnyFlag(@SystemUiStateFlags flags: Long, flagMask: Long): Boolean { return (flags and flagMask) != 0L } diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java index 5653d932cd..6e2c6daa6b 100644 --- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java +++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java @@ -15,6 +15,7 @@ */ package com.android.quickstep.util; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.Display.DEFAULT_DISPLAY; import android.content.Context; @@ -24,26 +25,43 @@ import android.view.DisplayCutout; import android.view.Surface; import android.view.WindowManager; import android.view.WindowMetrics; +import android.window.DesktopExperienceFlags; import com.android.internal.policy.SystemBarUtils; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.logging.FileLog; import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.util.WindowBounds; import com.android.launcher3.util.window.CachedDisplayInfo; import com.android.launcher3.util.window.WindowManagerProxy; -import com.android.quickstep.LauncherActivityInterface; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.fallback.window.RecentsWindowFlags; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopState; +import java.util.Collections; import java.util.List; import java.util.Set; -/** - * Extension of {@link WindowManagerProxy} with some assumption for the default - * system Launcher - */ -public class SystemWindowManagerProxy extends WindowManagerProxy { +import javax.inject.Inject; - public SystemWindowManagerProxy(Context context) { +/** + * Extension of {@link WindowManagerProxy} with some assumption for the default system Launcher + */ +@LauncherAppSingleton +public class SystemWindowManagerProxy extends WindowManagerProxy { + // LC-Note: This is pretty much unused by Launcher3, see [LawnchairWindowManagerProxy] + + private final DesktopVisibilityController mDesktopVisibilityController; + private final SystemUiProxy mSystemUiProxy; + + @Inject + public SystemWindowManagerProxy( + DesktopVisibilityController desktopVisibilityController, + SystemUiProxy systemUiProxy) { super(true); + mDesktopVisibilityController = desktopVisibilityController; + mSystemUiProxy = systemUiProxy; } @Override @@ -53,10 +71,61 @@ public class SystemWindowManagerProxy extends WindowManagerProxy { } @Override - public boolean isInDesktopMode() { - DesktopVisibilityController desktopController = LauncherActivityInterface.INSTANCE - .getDesktopVisibilityController(); - return desktopController != null && desktopController.areDesktopTasksVisible(); + public void registerDesktopVisibilityListener(DesktopVisibilityListener listener) { + mDesktopVisibilityController.registerDesktopVisibilityListener(listener); + } + + @Override + public void unregisterDesktopVisibilityListener(DesktopVisibilityListener listener) { + mDesktopVisibilityController.unregisterDesktopVisibilityListener(listener); + } + + @Override + public boolean isInDesktopMode(int displayId) { + return mDesktopVisibilityController.isInDesktopMode(displayId); + } + + @Override + public boolean isDisplayDesktopFirst(Context displayInfoContext) { + if (!DesktopState.fromContext(displayInfoContext).canEnterDesktopMode()) { + return false; + } + return displayInfoContext.getResources().getConfiguration() + .windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM; + } + + @Override + public boolean showLockedTaskbarOnHome(Context displayInfoContext) { + if (!DesktopModeStatus.canEnterDesktopMode(displayInfoContext)) { + return false; + } + if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(displayInfoContext)) { + return false; + } + + return isDisplayDesktopFirst(displayInfoContext); + } + + @Override + public boolean showDesktopTaskbarForFreeformDisplay(Context displayInfoContext) { + if (!DesktopModeStatus.canEnterDesktopMode(displayInfoContext)) { + return false; + } + + if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(displayInfoContext)) { + return false; + } + + if (!DesktopExperienceFlags.ENABLE_DESKTOP_TASKBAR_ON_FREEFORM_DISPLAYS.isTrue()) { + return false; + } + + return isDisplayDesktopFirst(displayInfoContext); + } + + @Override + public boolean isHomeVisible() { + return mSystemUiProxy.getHomeVisibilityState().isHomeVisible(); } @Override @@ -67,10 +136,8 @@ public class SystemWindowManagerProxy extends WindowManagerProxy { @Override protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) { - // See b/264656380, calculate the status bar height manually as the inset in the - // system - // server might not be updated by this point yet causing extra DeviceProfile - // updates + // See b/264656380, calculate the status bar height manually as the inset in the system + // server might not be updated by this point yet causing extra DeviceProfile updates return SystemBarUtils.getStatusBarHeight(context); } @@ -79,9 +146,14 @@ public class SystemWindowManagerProxy extends WindowManagerProxy { Context displayInfoContext) { ArrayMap> result = new ArrayMap<>(); WindowManager windowManager = displayInfoContext.getSystemService(WindowManager.class); - Set possibleMaximumWindowMetrics = windowManager - .getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); - FileLog.d("b/283944974", "possibleMaximumWindowMetrics: " + possibleMaximumWindowMetrics); + Set possibleMaximumWindowMetrics = + null; + try { + possibleMaximumWindowMetrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); + } catch (Throwable t) { + possibleMaximumWindowMetrics = Collections.singleton( + windowManager.getMaximumWindowMetrics()); + } for (WindowMetrics windowMetrics : possibleMaximumWindowMetrics) { CachedDisplayInfo info = getDisplayInfo(windowMetrics, Surface.ROTATION_0); List bounds = estimateWindowBounds(displayInfoContext, info); @@ -95,4 +167,9 @@ public class SystemWindowManagerProxy extends WindowManagerProxy { int fromRotation, int toRotation) { return original.getRotated(startWidth, startHeight, fromRotation, toRotation); } + + @Override + public boolean enableOverviewOnConnectedDisplays() { + return RecentsWindowFlags.enableOverviewOnConnectedDisplays(); + } } diff --git a/quickstep/src/com/android/quickstep/util/TISBindHelper.java b/quickstep/src/com/android/quickstep/util/TISBindHelper.java index 9a010429d3..027dc083cc 100644 --- a/quickstep/src/com/android/quickstep/util/TISBindHelper.java +++ b/quickstep/src/com/android/quickstep/util/TISBindHelper.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.util.Log; import androidx.annotation.Nullable; @@ -45,7 +46,7 @@ public class TISBindHelper implements ServiceConnection { // Max backoff caps at 5 mins private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000; - private final Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Runnable mConnectionRunnable = this::internalBindToTIS; private final Context mContext; private final Consumer mConnectionCallback; diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java deleted file mode 100644 index 98d363ef5c..0000000000 --- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.util; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import androidx.annotation.IntDef; - -import com.android.launcher3.util.IntArray; - -import java.lang.annotation.Retention; - -/** - * Helper class for navigating RecentsView grid tasks via arrow keys and tab. - */ -public class TaskGridNavHelper { - public static final int CLEAR_ALL_PLACEHOLDER_ID = -1; - public static final int INVALID_FOCUSED_TASK_ID = -1; - - public static final int DIRECTION_UP = 0; - public static final int DIRECTION_DOWN = 1; - public static final int DIRECTION_LEFT = 2; - public static final int DIRECTION_RIGHT = 3; - public static final int DIRECTION_TAB = 4; - - @Retention(SOURCE) - @IntDef({DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TAB}) - public @interface TASK_NAV_DIRECTION {} - - private final IntArray mOriginalTopRowIds; - private IntArray mTopRowIds; - private IntArray mBottomRowIds; - private final int mFocusedTaskId; - - public TaskGridNavHelper(IntArray topIds, IntArray bottomIds, int focusedTaskId) { - mFocusedTaskId = focusedTaskId; - mOriginalTopRowIds = topIds.clone(); - generateTaskViewIdGrid(topIds, bottomIds); - } - - private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray) { - boolean hasFocusedTask = mFocusedTaskId != INVALID_FOCUSED_TASK_ID; - int maxSize = - Math.max(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0); - int minSize = - Math.min(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0); - - // Add the focused task to the beginning of both arrays if it exists. - if (hasFocusedTask) { - topRowIdArray.add(0, mFocusedTaskId); - bottomRowIdArray.add(0, mFocusedTaskId); - } - - // Fill in the shorter array with the ids from the longer one. - for (int i = minSize; i < maxSize; i++) { - if (i >= topRowIdArray.size()) { - topRowIdArray.add(bottomRowIdArray.get(i)); - } else { - bottomRowIdArray.add(topRowIdArray.get(i)); - } - } - - // Add the clear all button to the end of both arrays - topRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID); - bottomRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID); - - mTopRowIds = topRowIdArray; - mBottomRowIds = bottomRowIdArray; - } - - /** - * Returns the id of the next page in the grid or -1 for the clear all button. - */ - public int getNextGridPage(int currentPageTaskViewId, int delta, - @TASK_NAV_DIRECTION int direction, boolean cycle) { - boolean inTop = mTopRowIds.contains(currentPageTaskViewId); - int index = inTop ? mTopRowIds.indexOf(currentPageTaskViewId) - : mBottomRowIds.indexOf(currentPageTaskViewId); - int maxSize = Math.max(mTopRowIds.size(), mBottomRowIds.size()); - int nextIndex = index + delta; - - switch (direction) { - case DIRECTION_UP: - case DIRECTION_DOWN: { - return inTop ? mBottomRowIds.get(index) : mTopRowIds.get(index); - } - case DIRECTION_LEFT: { - int boundedIndex = cycle ? nextIndex % maxSize : Math.min(nextIndex, maxSize - 1); - return inTop ? mTopRowIds.get(boundedIndex) - : mBottomRowIds.get(boundedIndex); - } - case DIRECTION_RIGHT: { - int boundedIndex = - cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex) : Math.max( - nextIndex, 0); - boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId); - return inOriginalTop ? mTopRowIds.get(boundedIndex) - : mBottomRowIds.get(boundedIndex); - } - case DIRECTION_TAB: { - int boundedIndex = - cycle ? nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize : Math.min( - nextIndex, maxSize - 1); - if (delta >= 0) { - return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index) - ? mBottomRowIds.get(index) - : mTopRowIds.get(boundedIndex); - } else { - if (mTopRowIds.contains(currentPageTaskViewId)) { - return mBottomRowIds.get(boundedIndex); - } else { - // Go up to top if there is task above - return mTopRowIds.get(index) != mBottomRowIds.get(index) - ? mTopRowIds.get(index) - : mBottomRowIds.get(boundedIndex); - } - } - } - default: - return currentPageTaskViewId; - } - } -} diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt new file mode 100644 index 0000000000..bf60b87b89 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util + +import com.android.launcher3.util.IntArray +import kotlin.math.abs +import kotlin.math.max + +/** Helper class for navigating RecentsView grid tasks via arrow keys and tab. */ +class TaskGridNavHelper( + private val topIds: IntArray, + bottomIds: IntArray, + largeTileIds: List, + hasAddDesktopButton: Boolean, +) { + private val topRowIds = mutableListOf() + private val bottomRowIds = mutableListOf() + + init { + // Add AddDesktopButton and lage tiles to both rows. + if (hasAddDesktopButton) { + topRowIds += ADD_DESK_PLACEHOLDER_ID + bottomRowIds += ADD_DESK_PLACEHOLDER_ID + } + topRowIds += largeTileIds + bottomRowIds += largeTileIds + + // Add row ids to their respective rows. + topRowIds += topIds + bottomRowIds += bottomIds + + // Fill in the shorter array with the ids from the longer one. + topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0)) + bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0)) + + // Add the clear all button to the end of both arrays. + topRowIds += CLEAR_ALL_PLACEHOLDER_ID + bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID + } + + /** Returns the id of the next page in the grid or -1 for the clear all button. */ + fun getNextGridPage( + currentPageTaskViewId: Int, + delta: Int, + direction: TaskNavDirection, + cycle: Boolean, + ): Int { + val inTop = topRowIds.contains(currentPageTaskViewId) + val inBottom = bottomRowIds.contains(currentPageTaskViewId) + if (!inTop && !inBottom) { + return currentPageTaskViewId + } + val index = + if (inTop) topRowIds.indexOf(currentPageTaskViewId) + else bottomRowIds.indexOf(currentPageTaskViewId) + val maxSize = max(topRowIds.size, bottomRowIds.size) + val nextIndex = index + delta + + return when (direction) { + TaskNavDirection.UP, + TaskNavDirection.DOWN -> { + if (inTop) bottomRowIds[index] else topRowIds[index] + } + TaskNavDirection.LEFT -> { + val boundedIndex = + if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1) + if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex] + } + TaskNavDirection.RIGHT -> { + val boundedIndex = + if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex) + else nextIndex.coerceAtLeast(0) + val inOriginalTop = topIds.contains(currentPageTaskViewId) + if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex] + } + TaskNavDirection.TAB -> { + val boundedIndex = + if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize) + else nextIndex.coerceAtMost(maxSize - 1) + if (delta >= 0) { + if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index] + else topRowIds[boundedIndex] + } else { + if (topRowIds.contains(currentPageTaskViewId)) { + if (boundedIndex < 0) { + // If no cycling, always return the first task. + topRowIds[0] + } else { + bottomRowIds[boundedIndex] + } + } else { + // Go up to top if there is task above + if (topRowIds[index] != bottomRowIds[index]) topRowIds[index] + else bottomRowIds[boundedIndex] + } + } + } + else -> currentPageTaskViewId + } + } + + /** + * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab + * navigation, starting from the initial TaskView ID, towards the start or end of the grid. + * + *

A positive delta moves forward in the tab order towards the end of the grid, while a + * negative value moves backward towards the beginning. The offset is the distance between + * columns the tasks are in. + */ + fun gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId: Int, + towardsStart: Boolean, + ): Sequence> = sequence { + val draggedTaskViewColumn = getColumn(initialTaskViewId) + var nextTaskViewId: Int = initialTaskViewId + var previousTaskViewId: Int = Int.MIN_VALUE + while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) { + previousTaskViewId = nextTaskViewId + nextTaskViewId = + getNextGridPage( + nextTaskViewId, + if (towardsStart) -1 else 1, + TaskNavDirection.TAB, + cycle = false, + ) + if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) { + val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn) + yield(Pair(nextTaskViewId, columnOffset)) + } + } + } + + /** Returns the column of a task's id in the grid. */ + private fun getColumn(taskViewId: Int): Int = + if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId) + else bottomRowIds.indexOf(taskViewId) + + enum class TaskNavDirection { + UP, + DOWN, + LEFT, + RIGHT, + TAB, + } + + companion object { + const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1 + const val ADD_DESK_PLACEHOLDER_ID: Int = -2 + } +} diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java index 69137cc1b6..43ef39c256 100644 --- a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java +++ b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java @@ -17,6 +17,7 @@ package com.android.quickstep.util; import android.util.Log; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.systemui.shared.recents.model.Task; @@ -94,6 +95,7 @@ public class TaskKeyByLastActiveTimeCache implements TaskKeyCache { * Gets the entry if it is still valid */ @Override + @Nullable public synchronized V getAndInvalidateIfModified(Task.TaskKey key) { Entry entry = mMap.get(key.id); if (entry != null && entry.mKey.windowingMode == key.windowingMode diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java index 8ee78ab0ba..9df0993a7b 100644 --- a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java +++ b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java @@ -15,6 +15,8 @@ */ package com.android.quickstep.util; +import androidx.annotation.Nullable; + import com.android.systemui.shared.recents.model.Task; import java.util.function.Predicate; @@ -44,6 +46,7 @@ public interface TaskKeyCache { /** * Gets the entry if it is still valid. */ + @Nullable V getAndInvalidateIfModified(Task.TaskKey key); /** diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java index 89f5d41dad..9fe8cc954d 100644 --- a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java +++ b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java @@ -17,6 +17,8 @@ package com.android.quickstep.util; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.systemui.shared.recents.model.Task.TaskKey; import java.util.LinkedHashMap; @@ -59,6 +61,7 @@ public class TaskKeyLruCache implements TaskKeyCache { /** * Gets the entry if it is still valid */ + @Nullable public synchronized V getAndInvalidateIfModified(TaskKey key) { Entry entry = mMap.get(key.id); diff --git a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java index e80d2a6d3f..40a328ccfc 100644 --- a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java +++ b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java @@ -98,10 +98,7 @@ public class TaskRemovedDuringLaunchListener { final Runnable taskLaunchFailedCallback = mTaskLaunchFailedCallback; RecentsModel.INSTANCE.get(mContext).isTaskRemoved(mLaunchedTaskId, (taskRemoved) -> { if (taskRemoved) { - ActiveGestureLog.INSTANCE.addLog( - new ActiveGestureLog.CompoundString("Launch failed, task (id=") - .append(launchedTaskId) - .append(") finished mid transition")); + ActiveGestureProtoLogProxy.logTaskLaunchFailed(launchedTaskId); taskLaunchFailedCallback.run(); } }, (task) -> true /* filter */); diff --git a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java index 91e8376990..6e2d469d18 100644 --- a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java +++ b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java @@ -16,16 +16,11 @@ package com.android.quickstep.util; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; - -import android.app.Activity; import android.app.ActivityManager; import android.util.Log; import androidx.annotation.NonNull; -import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter; -import com.android.quickstep.RecentsModel; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index fcf303fdd9..bb140676fd 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -17,17 +17,15 @@ package com.android.quickstep.util; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static com.android.launcher3.Flags.enableGridOnlyOverview; import static com.android.launcher3.states.RotationHelper.deltaRotation; import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; -import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition; -import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS; +import static com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview; import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation; import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation; -import static com.android.quickstep.util.SplitScreenUtils.convertLauncherSplitBoundsToShell; +import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import android.animation.TimeInterpolator; import android.content.Context; @@ -43,20 +41,21 @@ import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.app.animation.Interpolators; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.BaseActivityInterface; import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.DesktopFullscreenDrawParams; +import com.android.quickstep.FullscreenDrawParams; import com.android.quickstep.TaskAnimationManager; import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; -import com.android.quickstep.views.TaskView.FullscreenDrawParams; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; +import com.android.wm.shell.shared.split.SplitBounds; +import com.android.wm.shell.shared.split.SplitScreenConstants; /** * A utility class which emulates the layout behavior of TaskView and RecentsView @@ -83,8 +82,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private PointF mPivotOverride = null; private final PointF mPivot = new PointF(); private DeviceProfile mDp; - @StagePosition - private int mStagePosition = STAGE_POSITION_UNDEFINED; + @SplitScreenConstants.SplitPosition + private int mSplitPosition = SPLIT_POSITION_UNDEFINED; private final Matrix mMatrix = new Matrix(); private final Matrix mMatrixTmp = new Matrix(); @@ -99,11 +98,11 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private final FullscreenDrawParams mCurrentFullscreenParams; public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat(); public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat(); + public final AnimatedFloat taskGridTranslationX = new AnimatedFloat(); + public final AnimatedFloat taskGridTranslationY = new AnimatedFloat(); // Carousel properties public final AnimatedFloat carouselScale = new AnimatedFloat(); - public final AnimatedFloat carouselPrimaryTranslation = new AnimatedFloat(); - public final AnimatedFloat carouselSecondaryTranslation = new AnimatedFloat(); // RecentsView properties public final AnimatedFloat recentsViewScale = new AnimatedFloat(); @@ -117,20 +116,28 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private int mOrientationStateId; private SplitBounds mSplitBounds; private Boolean mDrawsBelowRecents = null; + private Boolean mDrawAboveOtherApps = null; private boolean mIsGridTask; - private boolean mIsDesktopTask; - private boolean mScaleToCarouselTaskSize = false; - private int mTaskRectTranslationX; - private int mTaskRectTranslationY; + private final boolean mIsDesktopTask; + private boolean mIsAnimatingToCarousel = false; + private final int mDesktopTaskIndex; - public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) { + @Nullable + private Matrix mTaskRectTransform = null; + + public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy, + boolean isDesktop, int desktopTaskIndex) { mContext = context; mSizeStrategy = sizeStrategy; + mIsDesktopTask = isDesktop; + mDesktopTaskIndex = desktopTaskIndex; mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init", () -> new RecentsOrientedState(context, sizeStrategy, i -> { })); mOrientationState.setGestureActive(true); - mCurrentFullscreenParams = new FullscreenDrawParams(context); + mCurrentFullscreenParams = mIsDesktopTask + ? new DesktopFullscreenDrawParams(context) + : new FullscreenDrawParams(context); mOrientationStateId = mOrientationState.getStateId(); Resources resources = context.getResources(); mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources); @@ -144,10 +151,16 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mDp = dp; mLayoutValid = false; mOrientationState.setDeviceProfile(dp); + if (enableGridOnlyOverview()) { + mIsGridTask = dp.getDeviceProperties().isTablet() && !mIsDesktopTask; + } calculateTaskSize(); } - private void calculateTaskSize() { + /** + * Updates the task size. + */ + public void calculateTaskSize() { if (mDp == null) { return; } @@ -155,14 +168,16 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { if (mIsGridTask) { mSizeStrategy.calculateGridTaskSize(mContext, mDp, mFullTaskSize, mOrientationState.getOrientationHandler()); + if (enableGridOnlyOverview()) { + mSizeStrategy.calculateTaskSize(mContext, mDp, mCarouselTaskSize, + mOrientationState.getOrientationHandler()); + } } else { mSizeStrategy.calculateTaskSize(mContext, mDp, mFullTaskSize, mOrientationState.getOrientationHandler()); - } - - if (enableGridOnlyOverview()) { - mSizeStrategy.calculateCarouselTaskSize(mContext, mDp, mCarouselTaskSize, - mOrientationState.getOrientationHandler()); + if (enableGridOnlyOverview()) { + mCarouselTaskSize.set(mFullTaskSize); + } } if (mSplitBounds != null) { @@ -171,8 +186,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { // sized task space bounds mTaskRect.set(mFullTaskSize); mOrientationState.getOrientationHandler() - .setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mStagePosition); - mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); + .setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mSplitPosition); } else if (mIsDesktopTask) { // For desktop, tasks can take up only part of the screen size. // Full task size represents the whole screen size, but scaled down to fit in recents. @@ -186,9 +200,17 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mTaskRect.scale(scale); // Ensure the task rect is inside the full task rect mTaskRect.offset(mFullTaskSize.left, mFullTaskSize.top); + + Rect taskDimension = new Rect(0, 0, (int) fullscreenTaskDimension.x, + (int) fullscreenTaskDimension.y); + mTmpCropRect.set(mThumbnailPosition); + if (mTmpCropRect.setIntersect(taskDimension, mThumbnailPosition)) { + mTmpCropRect.offset(-mThumbnailPosition.left, -mThumbnailPosition.top); + } else { + mTmpCropRect.setEmpty(); + } } else { mTaskRect.set(mFullTaskSize); - mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); } } @@ -207,16 +229,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { if (mDp == null) { return 1; } - // Copy mFullTaskSize instead of updating it directly so it could be reused next time - // without recalculating - Rect scaleRect = new Rect(); - if (mScaleToCarouselTaskSize) { - scaleRect.set(mCarouselTaskSize); - } else { - scaleRect.set(mFullTaskSize); - } - scaleRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); - float scale = mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot); + float scale = mOrientationState.getFullScreenScaleAndPivot( + mIsAnimatingToCarousel ? mCarouselTaskSize : mFullTaskSize, mDp, mPivot); if (mPivotOverride != null) { mPivot.set(mPivotOverride); } @@ -244,12 +258,13 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { setPreview(runningTarget); mSplitBounds = splitInfo; if (mSplitBounds == null) { - mStagePosition = STAGE_POSITION_UNDEFINED; + mSplitPosition = SPLIT_POSITION_UNDEFINED; } else { - mStagePosition = runningTarget.taskId == splitInfo.leftTopTaskId - ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; - mPositionHelper.setSplitBounds(convertLauncherSplitBoundsToShell(mSplitBounds), - mStagePosition); + mSplitPosition = runningTarget.taskId == splitInfo.leftTopTaskId + ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; + if (enableFlexibleTwoAppSplit()) { + mPositionHelper.setSplitBounds(mSplitBounds, mSplitPosition); + } } calculateTaskSize(); } @@ -277,6 +292,10 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mDrawsBelowRecents = drawsBelowRecents; } + public boolean getDrawsBelowRecents() { + return mDrawsBelowRecents != null ? mDrawsBelowRecents : false; + } + /** * Sets whether the task is part of overview grid and not being focused. */ @@ -285,83 +304,34 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { } /** - * Sets whether this task is part of desktop tasks in overview. + * Sets whether drawing this app above other apps during animation. It's currently used when + * activating an app window from the exploded desktop view which will launch the desktop tile + * and exit Overview. */ - public void setIsDesktopTask(boolean desktop) { - mIsDesktopTask = desktop; + public void setDrawsAboveOtherApps(boolean drawsAboveOtherApps) { + mDrawAboveOtherApps = drawsAboveOtherApps; } /** - * Apply translations on TaskRect's starting location. + * Override the pivot used to apply scale changes. */ - public void setTaskRectTranslation(int taskRectTranslationX, int taskRectTranslationY) { - mTaskRectTranslationX = taskRectTranslationX; - mTaskRectTranslationY = taskRectTranslationY; - // Re-calculate task size after changing translation - calculateTaskSize(); + public void setPivotOverride(PointF pivotOverride) { + mPivotOverride = pivotOverride; + getFullScreenScale(); } /** - * Adds animation for all the components corresponding to transition from an app to overview. + * Adds animation for all the components corresponding to transition from an app to carousel. */ - public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) { + public void addAppToCarouselAnim(PendingAnimation pa, Interpolator interpolator, + boolean isHandlingAtomicEvent) { pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator); - float fullScreenScale; - if (enableGridOnlyOverview() && mDp.isTablet && mDp.isGestureMode) { - // Move pivot to top right edge of the screen, to avoid task scaling down in opposite - // direction of app window movement, otherwise the animation will wiggle left and right. - // Also translate the app window to top right edge of the screen to simplify - // calculations. - taskPrimaryTranslation.value = mIsRecentsRtl - ? mDp.widthPx - mFullTaskSize.right - : -mFullTaskSize.left; - taskSecondaryTranslation.value = -mFullTaskSize.top; - mPivotOverride = new PointF(mIsRecentsRtl ? mDp.widthPx : 0, 0); - - // Scale down to the carousel and use the carousel Rect to calculate fullScreenScale. - mScaleToCarouselTaskSize = true; + if (enableGridOnlyOverview() && mDp.getDeviceProperties().isTablet() && !isHandlingAtomicEvent) { + mIsAnimatingToCarousel = true; carouselScale.value = mCarouselTaskSize.width() / (float) mFullTaskSize.width(); - fullScreenScale = getFullScreenScale(); - - float carouselPrimaryTranslationTarget = mIsRecentsRtl - ? mCarouselTaskSize.right - mDp.widthPx - : mCarouselTaskSize.left; - float carouselSecondaryTranslationTarget = mCarouselTaskSize.top; - - // Expected carousel position's center is in the middle, and invariant of - // recentsViewScale. - float exceptedCarouselCenterX = mCarouselTaskSize.centerX(); - // Animating carousel translations linearly will result in a curved path, therefore - // we'll need to calculate the expected translation at each recentsView scale. Luckily - // primary and secondary follow the same translation, and primary is used here due to - // it being simpler. - Interpolator carouselTranslationInterpolator = t -> { - // recentsViewScale is calculated rather than using recentsViewScale.value, so that - // this interpolator works independently even if recentsViewScale don't animate. - float recentsViewScale = - Utilities.mapToRange(t, 0, 1, fullScreenScale, 1, Interpolators.LINEAR); - // Without the translation, the app window will animate from fullscreen into top - // right corner. - float expectedTaskCenterX = mIsRecentsRtl - ? mDp.widthPx - mCarouselTaskSize.width() * recentsViewScale / 2f - : mCarouselTaskSize.width() * recentsViewScale / 2f; - // Calculate the expected translation, then work back the animatedFraction that - // results in this value. - float carouselPrimaryTranslation = - (exceptedCarouselCenterX - expectedTaskCenterX) / recentsViewScale; - return carouselPrimaryTranslation / carouselPrimaryTranslationTarget; - }; - - // Use addAnimatedFloat so this animation can later be canceled and animate to a - // different value in RecentsView.onPrepareGestureEndAnimation. - pa.addAnimatedFloat(carouselPrimaryTranslation, 0, carouselPrimaryTranslationTarget, - carouselTranslationInterpolator); - pa.addAnimatedFloat(carouselSecondaryTranslation, 0, carouselSecondaryTranslationTarget, - carouselTranslationInterpolator); - } else { - fullScreenScale = getFullScreenScale(); } - pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, fullScreenScale, 1, interpolator); + pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, + interpolator); } /** @@ -388,7 +358,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { public RectF getCurrentRect() { RectF result = getCurrentCropRect(); mMatrixTmp.set(mMatrix); - preDisplayRotation(mOrientationState.getDisplayRotation(), mDp.widthPx, mDp.heightPx, + preDisplayRotation(mOrientationState.getDisplayRotation(), mDp.getDeviceProperties().getWidthPx(), mDp.getDeviceProperties().getHeightPx(), mMatrixTmp); mMatrixTmp.mapRect(result); return result; @@ -405,16 +375,48 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { return mMatrix; } + /** + * Sets a matrix used to transform the position of tasks. If set, this matrix is applied to + * the task rect after the task has been scaled and positioned inside the fulltask, but + * before scaling and translation of the whole recents view is performed. + */ + public void setTaskRectTransform(@Nullable Matrix taskRectTransform) { + mTaskRectTransform = taskRectTransform; + } + + /** + * Calculates the crop rect for desktop tasks given the current matrix. + */ + private void calculateDesktopTaskCropRect() { + // The approach here is to map a rect that represents the untransformed thumbnail position + // using the current matrix. This will give us a rect that can be intersected with + // [mFullTaskSize]. Using the intersection, we then compute how much of the task window that + // needs to be cropped (which will be nothing if the window is entirely within the desktop). + mTempRectF.set(0, 0, mThumbnailPosition.width(), mThumbnailPosition.height()); + mMatrix.mapRect(mTempRectF); + + float offsetX = mTempRectF.left; + float offsetY = mTempRectF.top; + float scale = mThumbnailPosition.width() / mTempRectF.width(); + + if (mTempRectF.intersect(mFullTaskSize.left, mFullTaskSize.top, mFullTaskSize.right, + mFullTaskSize.bottom)) { + mTempRectF.offset(-offsetX, -offsetY); + mTempRectF.scale(scale); + mTempRectF.round(mTmpCropRect); + } + } + /** * Applies the rotation on the matrix to so that it maps from launcher coordinate space to * window coordinate space. */ public void applyWindowToHomeRotation(Matrix matrix) { - matrix.postTranslate(mDp.windowX, mDp.windowY); + matrix.postTranslate(mDp.getDeviceProperties().getWindowX(), mDp.getDeviceProperties().getWindowY()); postDisplayRotation(deltaRotation( mOrientationState.getRecentsActivityRotation(), mOrientationState.getDisplayRotation()), - mDp.widthPx, mDp.heightPx, matrix); + mDp.getDeviceProperties().getWidthPx(), mDp.getDeviceProperties().getHeightPx(), matrix); } /** @@ -449,7 +451,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { boolean isRtlEnabled = !mIsRecentsRtl; mPositionHelper.updateThumbnailMatrix( mThumbnailPosition, mThumbnailData, mTaskRect.width(), mTaskRect.height(), - mDp.isTablet, mOrientationState.getRecentsActivityRotation(), isRtlEnabled); + mDp.getDeviceProperties().isTablet(), mOrientationState.getRecentsActivityRotation(), isRtlEnabled); mPositionHelper.getMatrix().invert(mInversePositionMatrix); if (DEBUG) { Log.d(TAG, " taskRect: " + mTaskRect); @@ -466,18 +468,29 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mMatrix.set(mPositionHelper.getMatrix()); - // Apply TaskView matrix: taskRect, translate + // Apply TaskView matrix: taskRect, optional transform, translate mMatrix.postTranslate(mTaskRect.left, mTaskRect.top); + if (mTaskRectTransform != null) { + mMatrix.postConcat(mTaskRectTransform); + + // Calculate cropping for desktop tasks. The order is important since it uses the + // current matrix. Therefore we calculate it here, after applying the task rect + // transform, but before applying scaling/translation that affects the whole + // recentsview. + if (mIsDesktopTask) { + calculateDesktopTaskCropRect(); + } + } + mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE, taskPrimaryTranslation.value); mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE, taskSecondaryTranslation.value); + mMatrix.postTranslate(taskGridTranslationX.value, taskGridTranslationY.value); - mMatrix.postScale(carouselScale.value, carouselScale.value, mPivot.x, mPivot.y); - mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE, - carouselPrimaryTranslation.value); - mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE, - carouselSecondaryTranslation.value); + mMatrix.postScale(carouselScale.value, carouselScale.value, + mIsRecentsRtl ? mCarouselTaskSize.right : mCarouselTaskSize.left, + mCarouselTaskSize.top); mOrientationState.getOrientationHandler().setPrimary( mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value); @@ -490,10 +503,12 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { recentsViewPrimaryTranslation.value); applyWindowToHomeRotation(mMatrix); - // Crop rect is the inverse of thumbnail matrix - mTempRectF.set(0, 0, taskWidth, taskHeight); - mInversePositionMatrix.mapRect(mTempRectF); - mTempRectF.roundOut(mTmpCropRect); + if (!mIsDesktopTask) { + // Crop rect is the inverse of thumbnail matrix + mTempRectF.set(0, 0, taskWidth, taskHeight); + mInversePositionMatrix.mapRect(mTempRectF); + mTempRectF.roundOut(mTmpCropRect); + } params.setProgress(1f - fullScreenProgress); params.applySurfaceParams(surfaceTransaction == null @@ -511,8 +526,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { + " taskRect: " + mTaskRect + " taskPrimaryT: " + taskPrimaryTranslation.value + " taskSecondaryT: " + taskSecondaryTranslation.value - + " carouselPrimaryT: " + carouselPrimaryTranslation.value - + " carouselSecondaryT: " + carouselSecondaryTranslation.value + + " taskGridTranslationX: " + taskGridTranslationX.value + + " taskGridTranslationY: " + taskGridTranslationY.value + " recentsPrimaryT: " + recentsViewPrimaryTranslation.value + " recentsSecondaryT: " + recentsViewSecondaryTranslation.value + " recentsScroll: " + recentsViewScroll.value @@ -527,24 +542,22 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { .setWindowCrop(mTmpCropRect) .setCornerRadius(getCurrentCornerRadius()); - // If mDrawsBelowRecents is unset, no reordering will be enforced. - if (mDrawsBelowRecents != null) { - // In legacy transitions, the animation leashes remain in same hierarchy in the - // TaskDisplayArea, so we don't want to bump the layer too high otherwise it will - // conflict with layers that WM core positions (ie. the input consumers). For shell - // transitions, the animation leashes are reparented to an animation container so we - // can bump layers as needed. - if (ENABLE_SHELL_TRANSITIONS) { - builder.setLayer(mDrawsBelowRecents - ? Integer.MIN_VALUE + app.prefixOrderIndex - // 1000 is an arbitrary number to give room for multiple layers. - : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex); - } else { - builder.setLayer(mDrawsBelowRecents - ? Integer.MIN_VALUE + app.prefixOrderIndex - : 0); - } + if (mDrawsBelowRecents == null && mDrawAboveOtherApps == null) { + // No reordering will be enforced. + return; } + + // In shell transitions, the animation leashes are reparented to an animation container + // so we can bump layers as needed. + int baseLayer = app.prefixOrderIndex - mDesktopTaskIndex; + // 1000/2000 are arbitrary numbers to give room for multiple layers. + if (mDrawsBelowRecents != null) { + baseLayer += mDrawsBelowRecents ? Integer.MIN_VALUE + 2000 : Integer.MAX_VALUE - 2000; + } + if (mDrawAboveOtherApps != null && mDrawAboveOtherApps) { + baseLayer += 1000; + } + builder.setLayer(baseLayer); } /** @@ -552,7 +565,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { * TaskView */ public float getCurrentCornerRadius() { - float visibleRadius = mCurrentFullscreenParams.getCurrentDrawnCornerRadius(); + float visibleRadius = mCurrentFullscreenParams.getCurrentCornerRadius(); mTempPoint[0] = visibleRadius; mTempPoint[1] = 0; mInversePositionMatrix.mapVectors(mTempPoint); diff --git a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java index 66bff730bf..519ef60eca 100644 --- a/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java +++ b/quickstep/src/com/android/quickstep/util/TaskVisualsChangeListener.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; +import android.annotation.NonNull; import android.os.UserHandle; import com.android.systemui.shared.recents.model.Task; @@ -36,7 +37,7 @@ public interface TaskVisualsChangeListener { /** * Called when the icon for a task changes */ - default void onTaskIconChanged(String pkg, UserHandle user) {} + default void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {} /** * Called when the icon for a task changes diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java index 9bad1108bb..cb591ed035 100644 --- a/quickstep/src/com/android/quickstep/util/TransformParams.java +++ b/quickstep/src/com/android/quickstep/util/TransformParams.java @@ -19,9 +19,16 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import android.util.FloatProperty; import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import androidx.annotation.VisibleForTesting; import com.android.quickstep.RemoteAnimationTargets; import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; +import com.android.window.flags.Flags; + +import java.util.function.Supplier; public class TransformParams { @@ -56,15 +63,24 @@ public class TransformParams { private float mTargetAlpha; private float mCornerRadius; private RemoteAnimationTargets mTargetSet; + private TransitionInfo mTransitionInfo; + private boolean mCornerRadiusIsOverridden; private SurfaceTransactionApplier mSyncTransactionApplier; + private Supplier mSurfaceTransactionSupplier; private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE; private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE; public TransformParams() { + this(SurfaceTransaction::new); + } + + @VisibleForTesting + public TransformParams(Supplier surfaceTransactionSupplier) { mProgress = 0; mTargetAlpha = 1; mCornerRadius = -1; + mSurfaceTransactionSupplier = surfaceTransactionSupplier; } /** @@ -106,6 +122,15 @@ public class TransformParams { return this; } + /** + * Provides the {@code TransitionInfo} of the transition that this transformation stems from. + */ + public TransformParams setTransitionInfo(TransitionInfo transitionInfo) { + mTransitionInfo = transitionInfo; + mCornerRadiusIsOverridden = false; + return this; + } + /** * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that * are computed based on these TransformParams. @@ -136,26 +161,31 @@ public class TransformParams { /** Builds the SurfaceTransaction from the given BuilderProxy params. */ public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) { RemoteAnimationTargets targets = mTargetSet; - SurfaceTransaction transaction = new SurfaceTransaction(); + SurfaceTransaction transaction = mSurfaceTransactionSupplier.get(); if (targets == null) { return transaction; } for (int i = 0; i < targets.unfilteredApps.length; i++) { RemoteAnimationTarget app = targets.unfilteredApps[i]; SurfaceProperties builder = transaction.forSurface(app.leash); + BuilderProxy targetProxy = + app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME + ? mHomeBuilderProxy + : (app.mode == targets.targetMode ? proxy : mBaseBuilderProxy); if (app.mode == targets.targetMode) { - int activityType = app.windowConfiguration.getActivityType(); - if (activityType == ACTIVITY_TYPE_HOME) { - mHomeBuilderProxy.onBuildTargetParams(builder, app, this); - } else { - builder.setAlpha(getTargetAlpha()); - proxy.onBuildTargetParams(builder, app, this); - } - } else { - mBaseBuilderProxy.onBuildTargetParams(builder, app, this); + builder.setAlpha(getTargetAlpha()); + } + targetProxy.onBuildTargetParams(builder, app, this); + // Override the corner radius for {@code app} with the leash used by Shell, so that it + // doesn't interfere with the window clip and corner radius applied here. + // Only override the corner radius once - so that we don't accidentally override at the + // end of transition after WM Shell has reset the corner radius of the task. + if (!mCornerRadiusIsOverridden) { + overrideFreeformChangeLeashCornerRadiusToZero(app, transaction.getTransaction()); } } + mCornerRadiusIsOverridden = true; // always put wallpaper layer to bottom. final int wallpaperLength = targets.wallpapers != null ? targets.wallpapers.length : 0; @@ -166,7 +196,33 @@ public class TransformParams { return transaction; } - // Pubic getters so outside packages can read the values. + private void overrideFreeformChangeLeashCornerRadiusToZero( + RemoteAnimationTarget app, SurfaceControl.Transaction transaction) { + if (!Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + return; + } + if (app.taskInfo == null || !app.taskInfo.isFreeform()) { + return; + } + + SurfaceControl changeLeash = getChangeLeashForApp(app); + if (changeLeash != null) { + transaction.setCornerRadius(changeLeash, 0); + } + } + + private SurfaceControl getChangeLeashForApp(RemoteAnimationTarget app) { + if (mTransitionInfo == null) return null; + for (TransitionInfo.Change change : mTransitionInfo.getChanges()) { + if (change.getTaskInfo() == null) continue; + if (change.getTaskInfo().taskId == app.taskId) { + return change.getLeash(); + } + } + return null; + } + + // Public getters so outside packages can read the values. public float getProgress() { return mProgress; diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java index 0a97793b28..32e0e132e0 100644 --- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java +++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java @@ -30,6 +30,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.util.FloatProperty; +import android.util.Log; import android.view.View; import com.android.app.animation.Interpolators; @@ -51,6 +52,8 @@ import com.android.systemui.plugins.ResourceProvider; */ public class WorkspaceRevealAnim { + private static final String TAG = "WorkspaceRevealAnim"; + // Should be used for animations running alongside this WorkspaceRevealAnim. public static final int DURATION_MS = 350; private static final FloatProperty> WORKSPACE_SCALE_PROPERTY = @@ -97,6 +100,19 @@ public class WorkspaceRevealAnim { mAnimators.setDuration(DURATION_MS); mAnimators.setInterpolator(Interpolators.DECELERATED_EASE); + mAnimators.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + Log.d(TAG, "onAnimationCancel"); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + Log.d(TAG, "onAnimationEnd: workspace alpha = " + workspace.getAlpha()); + } + }); } private void addRevealAnimatorsForView(T v, FloatProperty scaleProperty) { diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt index 09563f5527..6fc450a01b 100644 --- a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt +++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt @@ -22,7 +22,6 @@ import com.android.launcher3.Alarm import com.android.launcher3.DeviceProfile import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener import com.android.launcher3.anim.PendingAnimation -import com.android.launcher3.config.FeatureFlags import com.android.launcher3.uioverrides.QuickstepLauncher import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener @@ -30,7 +29,7 @@ import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionPr /** Controls animations that are happening during unfolding foldable devices */ class LauncherUnfoldTransitionController( private val launcher: QuickstepLauncher, - private val progressProvider: ProxyUnfoldTransitionProvider + private val progressProvider: ProxyUnfoldTransitionProvider, ) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener { private var isTablet: Boolean? = null @@ -57,11 +56,7 @@ class LauncherUnfoldTransitionController( } override fun onDeviceProfileChanged(dp: DeviceProfile) { - if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { - return - } - - if (isTablet != null && dp.isTablet != isTablet) { + if (isTablet != null && dp.getDeviceProperties().isTablet != isTablet) { // We should preemptively start the animation only if: // - We changed to the unfolded screen // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't @@ -71,20 +66,24 @@ class LauncherUnfoldTransitionController( // if Launcher was not open during unfold, in this case we receive the configuration // change only after we went back to home screen and we don't want to start the // animation in this case. - if (dp.isTablet && progressProvider.isActive && !hasUnfoldTransitionStarted) { + if ( + dp.getDeviceProperties().isTablet && + progressProvider.isActive && + !hasUnfoldTransitionStarted + ) { // Preemptively start the unfold animation to make sure that we have drawn // the first frame of the animation before the screen gets unblocked onTransitionStarted() Trace.beginAsyncSection("$TAG#startedPreemptively", 0) timeoutAlarm.setAlarm(PREEMPTIVE_UNFOLD_TIMEOUT_MS) } - if (!dp.isTablet) { + if (!dp.getDeviceProperties().isTablet) { // Reset unfold transition status when folded hasUnfoldTransitionStarted = false } } - isTablet = dp.isTablet + isTablet = dp.getDeviceProperties().isTablet } override fun onTransitionStarted() { @@ -93,7 +92,7 @@ class LauncherUnfoldTransitionController( provider = this, factory = this::onPrepareUnfoldAnimation, duration = - 1000L // The expected duration for the animation. Then only comes to play if we have + 1000L, // The expected duration for the animation. Then only comes to play if we have // to run the animation ourselves in case sysui misses the end signal ) timeoutAlarm.cancelAlarm() @@ -119,7 +118,7 @@ class LauncherUnfoldTransitionController( launcher, isVertical, dp.displayInfo.currentSize, - anim + anim, ) } diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt new file mode 100644 index 0000000000..ff20378b8b --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt @@ -0,0 +1,156 @@ +/* + * 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.quickstep.views + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.FloatProperty +import android.widget.ImageButton +import androidx.core.animation.addListener +import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X +import com.android.launcher3.R +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.MultiPropertyDelegate +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.MultiValueAlpha +import com.android.quickstep.util.BorderAnimator +import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator + +/** + * Button for supporting multiple desktop sessions. The button will be next to the first TaskView + * inside overview, while clicking this button will create a new desktop session. + */ +class AddDesktopButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ImageButton(context, attrs) { + + private val addDeskButtonAlpha = MultiValueAlpha(this, Alpha.entries.size) + var contentAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.CONTENT) + var visibilityAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.VISIBILITY) + var clickAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.CLICK) + + private val multiTranslationX = + MultiPropertyFactory(this, VIEW_TRANSLATE_X, TranslationX.entries.size) { a: Float, b: Float + -> + a + b + } + var gridTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.GRID) + var offsetTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.OFFSET) + + private val focusBorderAnimator: BorderAnimator = + createSimpleBorderAnimator( + context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_size), + context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width), + this::getBorderBounds, + this, + context + .obtainStyledAttributes(attrs, R.styleable.AddDesktopButton) + .getColor( + R.styleable.AddDesktopButton_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + + var borderEnabled = false + set(value) { + if (field == value) { + return + } + field = value + focusBorderAnimator.setBorderVisibility(visible = field && isFocused, animated = true) + } + + @JvmOverloads + fun animateVisibility(toVisible: Boolean, onAnimationEndAction: Runnable? = null) { + val targetButtonAlpha = if (toVisible) 1f else 0f + val targetDrawableAlpha = if (toVisible) 255 else 0 + + val iconDrawable: Drawable = this.drawable.mutate() + val fadeDuration = + context.resources.getInteger(R.integer.add_desktop_button_fade_duration).toLong() + val fadeDelay = + context.resources.getInteger(R.integer.add_desktop_button_fade_delay).toLong() + + val buttonFadeAnimator = + ObjectAnimator.ofFloat(this, CLICK_ALPHA, targetButtonAlpha).apply { + duration = fadeDuration + startDelay = fadeDelay + interpolator = LINEAR + } + + val iconFadeAnimator = + ObjectAnimator.ofInt(iconDrawable, DRAWABLE_ALPHA, targetDrawableAlpha).apply { + duration = fadeDuration + interpolator = LINEAR + } + + AnimatorSet().apply { + playTogether(buttonFadeAnimator, iconFadeAnimator) + addListener(onEnd = { onAnimationEndAction?.run() }) + start() + } + } + + public override fun onFocusChanged( + gainFocus: Boolean, + direction: Int, + previouslyFocusedRect: Rect?, + ) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + if (borderEnabled) { + focusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true) + } + } + + private fun getBorderBounds(bounds: Rect) { + bounds.set(0, 0, width, height) + val outlinePadding = + context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_outline_padding) + bounds.inset(-outlinePadding, -outlinePadding) + } + + override fun draw(canvas: Canvas) { + focusBorderAnimator.drawBorder(canvas) + super.draw(canvas) + } + + companion object { + private enum class Alpha { + CONTENT, + VISIBILITY, + CLICK, + } + + private enum class TranslationX { + GRID, + OFFSET, + } + + @JvmField + val VISIBILITY_ALPHA: FloatProperty = + KFloatProperty(AddDesktopButton::visibilityAlpha) + + private val CLICK_ALPHA: FloatProperty = + KFloatProperty(AddDesktopButton::clickAlpha) + } +} diff --git a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java index 121d8ede11..166ed7916c 100644 --- a/quickstep/src/com/android/quickstep/views/AllAppsEduView.java +++ b/quickstep/src/com/android/quickstep/views/AllAppsEduView.java @@ -267,8 +267,8 @@ public class AllAppsEduView extends AbstractFloatingView { DeviceProfile grid = launcher.getDeviceProfile(); DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx); lp.ignoreInsets = true; - lp.leftMargin = (grid.widthPx - mWidthPx) / 2; - lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx; + lp.leftMargin = (grid.getDeviceProperties().getWidthPx() - mWidthPx) / 2; + lp.topMargin = grid.getDeviceProperties().getHeightPx() - grid.hotseatBarSizePx - mMaxHeightPx; setLayoutParams(lp); } diff --git a/quickstep/src/com/android/quickstep/views/BlurUtils.kt b/quickstep/src/com/android/quickstep/views/BlurUtils.kt new file mode 100644 index 0000000000..318a0437f3 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/BlurUtils.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle + +/** Applies blur either behind launcher surface or live tile app. */ +class BlurUtils(private val recentsView: RecentsView<*, *>) { + + private fun getLiveTileRemoteTargetHandles() = + if ( + recentsView.remoteTargetHandles != null && + recentsView.recentsAnimationController != null + ) + recentsView.remoteTargetHandles + else null + + private fun Array.setDrawBelowRecents(drawBelowRecents: Boolean) { + forEach { it.taskViewSimulator.drawsBelowRecents = drawBelowRecents } + } + + /** + * Controls if live tile should be above or below Recents layer, and update the base layer to + * apply blur to in BaseDepthController. + */ + fun setDrawLiveTileBelowRecents(drawBelowRecents: Boolean) { + getLiveTileRemoteTargetHandles()?.setDrawBelowRecents(drawBelowRecents) + updateBlurLayer() + } + + /** + * Set surface in [remoteTargetHandles] to be above Recents layer, and update the base layer to + * apply blur to in BaseDepthController. + */ + fun setDrawAboveRecents(remoteTargetHandles: Array) { + remoteTargetHandles.setDrawBelowRecents(false) + updateBlurLayer(drawingAboveRecents = true) + } + + private fun updateBlurLayer(drawingAboveRecents: Boolean = false) { + if (!enableOverviewBackgroundWallpaperBlur()) return + // Blurs behind lowest live tile surface that's below recents or Launcher if there + // are none. + recentsView.depthController?.setBaseSurfaceOverride( + getLiveTileRemoteTargetHandles() + ?.asSequence() + ?.filter { it.taskViewSimulator.drawsBelowRecents } + ?.flatMap { it.transformParams.targetSet.apps.asIterable() } + ?.map { it.leash } + ?.maxByOrNull { it.layerId }, + /* applyOnDraw= */ drawingAboveRecents, + ) + } +} diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.kt b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt new file mode 100644 index 0000000000..48aa999b3e --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.util.AttributeSet +import android.util.FloatProperty +import android.widget.Button +import com.android.launcher3.Flags.enableFocusOutline +import com.android.launcher3.R +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.MultiPropertyDelegate +import com.android.launcher3.util.MultiValueAlpha +import com.android.quickstep.util.BorderAnimator +import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator +import kotlin.math.abs +import kotlin.math.min + +class ClearAllButton +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : Button(context, attrs, defStyleAttr, defStyleRes) { + + private val clearAllButtonAlpha = + object : MultiValueAlpha(this, Alpha.entries.size) { + override fun apply(value: Float) { + super.apply(value) + isClickable = value >= 1f + } + } + var scrollAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.SCROLL) + var contentAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.CONTENT) + var visibilityAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.VISIBILITY) + var dismissAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.DISMISS) + + var fullscreenProgress = 1f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + /** + * Moves ClearAllButton between carousel and 2 row grid. + * + * 0 = carousel; 1 = 2 row grid. + */ + var gridProgress = 1f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + private var normalTranslationPrimary = 0f + var fullscreenTranslationPrimary = 0f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + var gridTranslationPrimary = 0f + set(value) { + if (field == value) { + return + } + field = value + applyPrimaryTranslation() + } + + /** Used to put the button at the middle in the secondary coordinate. */ + var taskAlignmentTranslationY = 0f + set(value) { + if (field == value) { + return + } + field = value + applySecondaryTranslation() + } + + var gridScrollOffset = 0f + var scrollOffsetPrimary = 0f + + private var sidePadding = 0 + var borderEnabled = false + set(value) { + if (field == value) { + return + } + field = value + focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) + } + + private val focusBorderAnimator: BorderAnimator? = + if (enableFocusOutline()) + createSimpleBorderAnimator( + context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_radius), + context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width), + this::getBorderBounds, + this, + context + .obtainStyledAttributes(attrs, R.styleable.ClearAllButton) + .getColor( + R.styleable.ClearAllButton_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + else null + + private fun getBorderBounds(bounds: Rect) { + bounds.set(0, 0, width, height) + val outlinePadding = + context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_padding) + // Make the value negative to form a padding between button and outline + bounds.inset(-outlinePadding, -outlinePadding) + } + + public override fun onFocusChanged( + gainFocus: Boolean, + direction: Int, + previouslyFocusedRect: Rect?, + ) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + if (borderEnabled) { + focusBorderAnimator?.setBorderVisibility(gainFocus, /* animated= */ true) + } + } + + override fun draw(canvas: Canvas) { + focusBorderAnimator?.drawBorder(canvas) + super.draw(canvas) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + sidePadding = + recentsView?.let { it.pagedOrientationHandler?.getClearAllSidePadding(it, isLayoutRtl) } + ?: 0 + } + + private val recentsView: RecentsView<*, *>? + get() = parent as? RecentsView<*, *>? + + override fun hasOverlappingRendering() = false + + fun onRecentsViewScroll(scroll: Int, gridEnabled: Boolean) { + val recentsView = recentsView ?: return + + val orientationSize = + recentsView.pagedOrientationHandler.getPrimaryValue(width, height).toFloat() + if (orientationSize == 0f) { + return + } + + val clearAllScroll = recentsView.clearAllScroll + val adjustedScrollFromEdge = abs((scroll - clearAllScroll)).toFloat() + val shift = min(adjustedScrollFromEdge, orientationSize) + normalTranslationPrimary = if (isLayoutRtl) -shift else shift + if (!gridEnabled) { + normalTranslationPrimary += sidePadding.toFloat() + } + applyPrimaryTranslation() + applySecondaryTranslation() + var clearAllSpacing = recentsView.pageSpacing + recentsView.clearAllExtraPageSpacing + clearAllSpacing = if (isLayoutRtl) -clearAllSpacing else clearAllSpacing + scrollAlpha = + ((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing.toFloat()).coerceAtLeast( + 0f + ) + } + + fun getScrollAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean): Float { + var scrollAdjustment = 0f + if (fullscreenEnabled) { + scrollAdjustment += fullscreenTranslationPrimary + } + if (gridEnabled) { + scrollAdjustment += gridTranslationPrimary + gridScrollOffset + } + scrollAdjustment += scrollOffsetPrimary + return scrollAdjustment + } + + fun getOffsetAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean) = + getScrollAdjustment(fullscreenEnabled, gridEnabled) + + private fun applyPrimaryTranslation() { + val recentsView = recentsView ?: return + val orientationHandler = recentsView.pagedOrientationHandler + orientationHandler.primaryViewTranslate.set( + this, + (orientationHandler.getPrimaryValue(0f, taskAlignmentTranslationY) + + normalTranslationPrimary + + getFullscreenTrans(fullscreenTranslationPrimary) + + getGridTrans(gridTranslationPrimary)), + ) + } + + private fun applySecondaryTranslation() { + val recentsView = recentsView ?: return + val orientationHandler = recentsView.pagedOrientationHandler + orientationHandler.secondaryViewTranslate.set( + this, + orientationHandler.getSecondaryValue(0f, taskAlignmentTranslationY), + ) + } + + private fun getFullscreenTrans(endTranslation: Float) = + if (fullscreenProgress > 0) endTranslation else 0f + + private fun getGridTrans(endTranslation: Float) = if (gridProgress > 0) endTranslation else 0f + + companion object { + private enum class Alpha { + SCROLL, + CONTENT, + VISIBILITY, + DISMISS, + } + + @JvmField + val VISIBILITY_ALPHA: FloatProperty = + KFloatProperty(ClearAllButton::visibilityAlpha) + + @JvmField + val DISMISS_ALPHA: FloatProperty = + KFloatProperty(ClearAllButton::dismissAlpha) + } +} diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt new file mode 100644 index 0000000000..4d179f5446 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt @@ -0,0 +1,54 @@ +/* + * 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.quickstep.views + +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.FrameLayout + +class DesktopTaskContentView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { + var cornerRadius: Float = 0f + set(value) { + field = value + invalidateOutline() + } + + private val bounds = Rect() + + init { + clipToOutline = true + clipChildren = false + outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(bounds, cornerRadius) + } + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + bounds.set(0, 0, w, h) + invalidateOutline() + } +} diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt index c56d7db5c6..a6b9d9adec 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -15,271 +15,720 @@ */ package com.android.quickstep.views +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.annotation.SuppressLint import android.content.Context -import android.graphics.Point +import android.graphics.Matrix import android.graphics.PointF import android.graphics.Rect -import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.ShapeDrawable -import android.graphics.drawable.shapes.RoundRectShape +import android.graphics.Rect.intersects +import android.graphics.RectF import android.util.AttributeSet +import android.util.FloatProperty import android.util.Log +import android.util.Size +import android.view.Display.INVALID_DISPLAY +import android.view.Gravity import android.view.View -import android.view.ViewGroup +import android.view.ViewStub +import androidx.core.content.res.ResourcesCompat import androidx.core.view.updateLayoutParams -import app.lawnchair.theme.color.tokens.ColorTokens +import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix +import com.android.launcher3.Flags.enableDesktopExplodedView +import com.android.launcher3.Flags.enableRefactorTaskContentView +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.R +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.testing.TestLogging +import com.android.launcher3.testing.shared.TestProtocol +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu import com.android.launcher3.Utilities import com.android.launcher3.util.RunnableList import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.TransformingTouchDelegate import com.android.launcher3.util.ViewPool +import com.android.launcher3.util.rects.lerpRect import com.android.launcher3.util.rects.set import com.android.quickstep.BaseContainerInterface +import com.android.quickstep.DesktopFullscreenDrawParams +import com.android.quickstep.FullscreenDrawParams +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.ViewUtils +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.recents.di.get +import com.android.quickstep.recents.domain.model.DesktopLayoutConfig +import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData +import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.thumbnail.TaskContentView +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.util.DesktopTask import com.android.quickstep.util.RecentsOrientedState -import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops +import kotlin.math.roundToInt + +import app.lawnchair.theme.color.tokens.ColorTokens /** TaskView that contains all tasks that are part of the desktop. */ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - TaskView(context, attrs) { + TaskView( + context, + attrs, + type = TaskViewType.DESKTOP, + thumbnailFullscreenParams = DesktopFullscreenDrawParams(context), + ) { + private val desktopTask: DesktopTask? + get() = groupTask as? DesktopTask + + val deskId + get() = desktopTask?.deskId ?: DesktopVisibilityController.INACTIVE_DESK_ID + + private val contentViewFullscreenParams = FullscreenDrawParams(context) + + private val taskThumbnailViewDeprecatedPool = + if (!enableRefactorTaskThumbnail()) { + ViewPool( + context, + this, + R.layout.task_thumbnail_deprecated, + VIEW_POOL_MAX_SIZE, + VIEW_POOL_INITIAL_SIZE, + ) + } else null - private val snapshotDrawParams = - object : FullscreenDrawParams(context) { - // DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress. - override fun computeTaskCornerRadius(context: Context) = - computeWindowCornerRadius(context) - } private val taskThumbnailViewPool = - ViewPool( - context, - this, - R.layout.task_thumbnail, - VIEW_POOL_MAX_SIZE, - VIEW_POOL_INITIAL_SIZE - ) + if (enableRefactorTaskThumbnail()) { + ViewPool( + context, + this, + R.layout.task_thumbnail, + VIEW_POOL_MAX_SIZE, + VIEW_POOL_INITIAL_SIZE, + ) + } else null + + private val taskContentViewPool = + if (enableRefactorTaskContentView()) { + ViewPool( + context, + this, + R.layout.task_content_view, + VIEW_POOL_MAX_SIZE, + VIEW_POOL_INITIAL_SIZE, + ) + } else null + private val tempPointF = PointF() - private val tempRect = Rect() - private lateinit var backgroundView: View + private val lastComputedTaskSize = Rect() private lateinit var iconView: TaskViewIcon - private var childCountAtInflation = 0 + private lateinit var contentView: DesktopTaskContentView + private lateinit var backgroundView: View + + private var viewModel: DesktopTaskViewModel? = null + + /** + * Holds the default (user placed) positions of task windows. This can be moved into the + * viewModel once RefactorTaskThumbnail has been launched. + */ + private var fullscreenTaskPositions: List = emptyList() + + /** + * Holds the previous organized task positions. This is used to animate between two sets of + * organized task positions when a task is being dismissed. + */ + private var previousOrganizedDesktopTaskPositions: List? = null + + /** + * When enableDesktopExplodedView is enabled, this controls the gradual transition from the + * default positions to the organized non-overlapping positions. + */ + var explodeProgress = 0.0f + set(value) { + field = value + positionTaskWindows() + } + + /** + * When enableDesktopExplodedView is enabled, and a task is removed, this controls the gradual + * transition from the previous organized task positions to the new. + */ + private var taskRemoveProgress = 0.0f + set(value) { + field = value + positionTaskWindows() + } + + private var taskRemoveAnimator: ObjectAnimator? = null + + var remoteTargetHandles: Array? = null + set(value) { + field = value + if (value != null) { + positionTaskWindows() + } + } + + override val displayId: Int + get() = + if (enableMultipleDesktops(context)) { + desktopTask?.displayId ?: INVALID_DISPLAY + } else { + super.displayId + } + + private fun getRemoteTargetHandle(taskId: Int): RemoteTargetHandle? = + remoteTargetHandles?.firstOrNull { + it.transformParams.targetSet.firstAppTargetTaskId == taskId + } override fun onFinishInflate() { super.onFinishInflate() - backgroundView = - findViewById(R.id.background)!!.apply { + contentView = + findViewById(R.id.desktop_content).apply { updateLayoutParams { - topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx + topMargin = container.deviceProfile.overviewProfile.taskThumbnailTopMarginPx } - background = - ShapeDrawable(RoundRectShape(FloatArray(8) { taskCornerRadius }, null, null)) - .apply { - setTint( - ColorTokens.Neutral2_300.resolveColor(context) - ) - } + cornerRadius = contentViewFullscreenParams.currentCornerRadius + backgroundView = findViewById(R.id.background) + backgroundView.setBackgroundColor( + resources.getColor(ColorTokens.Neutral2_300.resolveColor(context), context.theme) + ) } - iconView = - getOrInflateIconView(R.id.icon).apply { - val iconBackground = resources.getDrawable(R.drawable.bg_circle, context.theme) - val icon = resources.getDrawable(R.drawable.ic_desktop, context.theme) - setIcon(this, LayerDrawable(arrayOf(iconBackground, icon))) - } - childCountAtInflation = childCount } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val containerWidth = MeasureSpec.getSize(widthMeasureSpec) - var containerHeight = MeasureSpec.getSize(heightMeasureSpec) - setMeasuredDimension(containerWidth, containerHeight) + override fun inflateViewStubs() { + findViewById(R.id.icon) + ?.apply { + layoutResource = + if (enableOverviewIconMenu()) R.layout.icon_app_chip_view + else R.layout.icon_view + } + ?.inflate() + } + private fun positionTaskWindows(updateLayout: Boolean = false) { if (taskContainers.isEmpty()) { return } - val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx - containerHeight -= thumbnailTopMarginPx - - BaseContainerInterface.getTaskDimension(context, container.deviceProfile, tempPointF) - val windowWidth = tempPointF.x.toInt() - val windowHeight = tempPointF.y.toInt() - val scaleWidth = containerWidth / windowWidth.toFloat() - val scaleHeight = containerHeight / windowHeight.toFloat() - if (DEBUG) { - Log.d( - TAG, - "onMeasure: container=[$containerWidth,$containerHeight] " + - "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]" - ) - } - - // Desktop tile is a shrunk down version of launcher and freeform task thumbnails. - taskContainers.forEach { - // Default to quarter of the desktop if we did not get app bounds. - val taskSize = - it.task.appBounds - ?: tempRect.apply { - left = 0 - top = 0 - right = windowWidth / 4 - bottom = windowHeight / 4 + val (widthScale, heightScale) = getScreenScaleFactors() + taskContainers.forEach { taskContainer -> + val taskId = taskContainer.task.key.id + val fullscreenTaskBounds = + fullscreenTaskPositions.firstOrNull { it.taskId == taskId }?.bounds ?: return + val overviewTaskBounds = + if (enableDesktopExplodedView()) { + viewModel!! + .organizedDesktopTaskPositions + .firstOrNull { it.taskId == taskId } + ?.bounds ?: fullscreenTaskBounds + } else { + fullscreenTaskBounds + } + val currentTaskBounds = + if (enableDesktopExplodedView()) { + TEMP_OVERVIEW_TASK_POSITION.apply { + // When removing a task, interpolate between its old organized bounds and + // [overviewTaskBounds]. + val previousOrganizedTaskBounds = + previousOrganizedDesktopTaskPositions + ?.firstOrNull { it.taskId == taskId } + ?.bounds + if (previousOrganizedTaskBounds != null) { + lerpRect( + previousOrganizedTaskBounds, + overviewTaskBounds, + taskRemoveProgress, + ) + } else { + set(overviewTaskBounds) + } + lerpRect(fullscreenTaskBounds, this, explodeProgress) } - val thumbWidth = (taskSize.width() * scaleWidth).toInt() - val thumbHeight = (taskSize.height() * scaleHeight).toInt() - it.thumbnailViewDeprecated.measure( - MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY) - ) + } else { + fullscreenTaskBounds + } - // Position the task to the same position as it would be on the desktop - val positionInParent = it.task.positionInParent ?: ORIGIN - val taskX = (positionInParent.x * scaleWidth).toInt() - var taskY = (positionInParent.y * scaleHeight).toInt() - // move task down by margin size - taskY += thumbnailTopMarginPx - it.thumbnailViewDeprecated.x = taskX.toFloat() - it.thumbnailViewDeprecated.y = taskY.toFloat() - if (DEBUG) { - Log.d( - TAG, - "onMeasure: task=${it.task.key} thumb=[$thumbWidth,$thumbHeight]" + - " pos=[$taskX,$taskY]" + if (enableDesktopExplodedView()) { + getRemoteTargetHandle(taskId)?.let { remoteTargetHandle -> + val fromRect = + TEMP_FROM_RECTF.apply { + set(fullscreenTaskBounds) + scale(widthScale) + offset( + lastComputedTaskSize.left.toFloat(), + lastComputedTaskSize.top.toFloat(), + ) + } + val toRect = + TEMP_TO_RECTF.apply { + set(currentTaskBounds) + scale(widthScale) + offset( + lastComputedTaskSize.left.toFloat(), + lastComputedTaskSize.top.toFloat(), + ) + } + val transform = Matrix() + transform.setRectToRect(fromRect, toRect, Matrix.ScaleToFit.FILL) + remoteTargetHandle.taskViewSimulator.setTaskRectTransform(transform) + remoteTargetHandle.taskViewSimulator.apply(remoteTargetHandle.transformParams) + } + + (taskContainer.taskContentView as? TaskContentView)?.setTaskHeaderAlpha( + explodeProgress ) } + + val overviewTaskLeft = overviewTaskBounds.left * widthScale + val overviewTaskTop = overviewTaskBounds.top * heightScale + val overviewTaskWidth = overviewTaskBounds.width() * widthScale + val overviewTaskHeight = overviewTaskBounds.height() * heightScale + + if (updateLayout) { + // Position the task to the same position as it would be on the desktop + taskContainer.taskContentView.updateLayoutParams { + gravity = Gravity.LEFT or Gravity.TOP + width = overviewTaskWidth.toInt() + height = overviewTaskHeight.toInt() + leftMargin = overviewTaskLeft.toInt() + topMargin = overviewTaskTop.toInt() + } + } + + if (enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail()) { + // When exploded view is disabled, these scale factors will be 1.0. This secondary + // scale factor is needed because a scale transform is applied to the thumbnail. + val thumbnailScaleWidth = + overviewTaskBounds.width().toFloat() / currentTaskBounds.width() + val thumbnailScaleHeight = + overviewTaskBounds.height().toFloat() / currentTaskBounds.height() + val screenRect = getScreenRect() + val contentOutlineBounds = + if (intersects(currentTaskBounds, screenRect)) + Rect(currentTaskBounds).apply { + intersectUnchecked(screenRect) + // Offset to 0,0 to transform into TaskThumbnailView's coordinate + // system. + offset(-currentTaskBounds.left, -currentTaskBounds.top) + left = (left * widthScale * thumbnailScaleWidth).roundToInt() + top = (top * heightScale * thumbnailScaleHeight).roundToInt() + right = (right * widthScale * thumbnailScaleWidth).roundToInt() + bottom = (bottom * heightScale * thumbnailScaleHeight).roundToInt() + } + else null + + if (enableRefactorTaskContentView()) { + (taskContainer.taskContentView as TaskContentView).outlineBounds = + contentOutlineBounds + } else { + taskContainer.thumbnailView.outlineBounds = contentOutlineBounds + } + } + + val currentTaskLeft = currentTaskBounds.left * widthScale + val currentTaskTop = currentTaskBounds.top * heightScale + val currentTaskWidth = currentTaskBounds.width() * widthScale + val currentTaskHeight = currentTaskBounds.height() * heightScale + // During the animation, apply translation and scale such that the view is transformed + // to where we want, without triggering layout. + taskContainer.taskContentView.apply { + pivotX = 0.0f + pivotY = 0.0f + translationX = currentTaskLeft - overviewTaskLeft + translationY = currentTaskTop - overviewTaskTop + scaleX = if (overviewTaskWidth != 0f) currentTaskWidth / overviewTaskWidth else 1f + scaleY = + if (overviewTaskHeight != 0f) currentTaskHeight / overviewTaskHeight else 1f + } + + if (taskContainer.task.isMinimized) { + taskContainer.taskContentView.alpha = explodeProgress + } } } - override fun onRecycle() { - super.onRecycle() - visibility = VISIBLE - } - /** Updates this desktop task to the gives task list defined in `tasks` */ fun bind( - tasks: List, + desktopTask: DesktopTask, orientedState: RecentsOrientedState, - taskOverlayFactory: TaskOverlayFactory + taskOverlayFactory: TaskOverlayFactory, ) { + this.groupTask = desktopTask + // Minimized tasks are shown in Overview when exploded view is enabled. + val tasks = + if (enableDesktopExplodedView()) { + desktopTask.tasks + } else { + desktopTask.tasks.filterNot { it.isMinimized } + } if (DEBUG) { val sb = StringBuilder() sb.append("bind tasks=").append(tasks.size).append("\n") tasks.forEach { sb.append(" key=${it.key}\n") } Log.d(TAG, sb.toString()) } - cancelPendingLoadTasks() - if (!isTaskContainersInitialized()) { - taskContainers = arrayListOf() - } - val taskContainers = taskContainers as ArrayList - taskContainers.ensureCapacity(tasks.size) - tasks.forEachIndexed { index, task -> - val thumbnailViewDeprecated: TaskThumbnailViewDeprecated - if (index >= taskContainers.size) { - thumbnailViewDeprecated = taskThumbnailViewPool.view - // Add thumbnailView from to position after the initial child views. - addView( - thumbnailViewDeprecated, - childCountAtInflation, - LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) + iconView = + (findViewById(R.id.icon) as TaskViewIcon).apply { + setIcon( + this, + ResourcesCompat.getDrawable( + context.resources, + R.drawable.ic_desktop_with_bg, + context.theme, + ), ) - } else { - thumbnailViewDeprecated = taskContainers[index].thumbnailViewDeprecated + setText(resources.getText(R.string.recent_task_desktop)) } - val taskContainer = - TaskContainer( - task, - // TODO(b/338360089): Support new TTV for DesktopTaskView - thumbnailView = null, - thumbnailViewDeprecated, - iconView, - TransformingTouchDelegate(iconView.asView()), - SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, - digitalWellBeingToast = null, - showWindowsView = null, - taskOverlayFactory - ) - .apply { thumbnailViewDeprecated.bind(task, overlay) } - if (index >= taskContainers.size) { - taskContainers.add(taskContainer) - } else { - taskContainers[index] = taskContainer - } - } - repeat(taskContainers.size - tasks.size) { - if (Utilities.ATLEAST_T) { - with(taskContainers.removeLast()) { - removeView(thumbnailViewDeprecated) - taskThumbnailViewPool.recycle(thumbnailViewDeprecated) - } - } else { - taskContainers.removeAt(taskContainers.lastIndex) - } - } - setOrientationState(orientedState) + cancelPendingLoadTasks() + val backgroundViewIndex = contentView.indexOfChild(backgroundView) + taskContainers = + tasks.map { task -> + val taskContentView = + when { + enableRefactorTaskContentView() -> taskContentViewPool!!.view + enableRefactorTaskThumbnail() -> taskThumbnailViewPool!!.view + else -> taskThumbnailViewDeprecatedPool!!.view + } + contentView.addView(taskContentView, backgroundViewIndex + 1) + val snapshotView = + if (enableRefactorTaskContentView()) { + taskContentView.findViewById(R.id.snapshot) + } else { + taskContentView + } + if (enableDesktopExplodedView()) { + taskContentView.setOnClickListener { + launchTaskWithDesktopController(animated = true, task.key.id) + } + if (taskContentView is TaskContentView) { + taskContentView.isFocusable = true + taskContentView.isHoverable = true + } + } + + TaskContainer( + this, + task, + taskContentView, + snapshotView, + iconView, + TransformingTouchDelegate(iconView.asView()), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + taskOverlayFactory, + ) + } + onBind(orientedState) } - override fun needsUpdate(dataChange: Int, flag: Int) = - if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false + override fun onBind(orientedState: RecentsOrientedState) { + super.onBind(orientedState) + + if (enableRefactorTaskThumbnail()) { + viewModel = + DesktopTaskViewModel(organizeDesktopTasksUseCase = RecentsDependencies.get(context)) + } + } + + override fun onRecycle() { + super.onRecycle() + explodeProgress = 0.0f + taskRemoveProgress = 0.0f + previousOrganizedDesktopTaskPositions = null + viewModel = null + visibility = VISIBLE + taskContainers.forEach { removeAndRecycleThumbnailView(it) } + if (enableOverviewIconMenu()) { + (iconView as IconAppChipView).reset() + } + remoteTargetHandles = null + } + + override fun setOrientationState(orientationState: RecentsOrientedState) { + super.setOrientationState(orientationState) + iconView.setIconOrientation(orientationState, isGridTask) + } + + @SuppressLint("RtlHardcoded") + override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) { + super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize) + this.lastComputedTaskSize.set(lastComputedTaskSize) + + updateTaskPositions() + } + + override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) { + super.onTaskListVisibilityChanged(visible, changes) + if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { + contentViewFullscreenParams.updateCornerRadius(context) + } + } + + override fun onIconLoaded(taskContainer: TaskContainer) { + // Update contentDescription of snapshotView only, individual task icon is unused. + taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription + } + + override fun setIconState(container: TaskContainer, state: TaskData?) { + container.snapshotView.contentDescription = (state as? TaskData.Data)?.titleDescription + } + + // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon + override fun onIconUnloaded(taskContainer: TaskContainer) {} // thumbnailView is laid out differently and is handled in onMeasure override fun updateThumbnailSize() {} override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) { if (relativeToDragLayer) { - container.dragLayer.getDescendantRectRelativeToSelf(backgroundView, bounds) + container.dragLayer.getDescendantRectRelativeToSelf(contentView, bounds) } else { - bounds.set(backgroundView) + bounds.set(contentView) } } - override fun launchTaskAnimated(): RunnableList? { + /** + * Launches the desktop task and activate the task with [taskIdToReorderToFront] if it's + * provided and already on the desktop. It will exit Overview to desktop and activate the + * according new task afterwards if applicable. + */ + private fun launchTaskWithDesktopController( + animated: Boolean, + taskIdToReorderToFront: Int? = null, + ): RunnableList? { val recentsView = recentsView ?: return null + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "launchDesktopFromRecents", + taskIds.contentToString(), + ) val endCallback = RunnableList() val desktopController = recentsView.desktopRecentsController checkNotNull(desktopController) { "recentsController is null" } - desktopController.launchDesktopFromRecents(this) { endCallback.executeAllAndDestroy() } - Log.d(TAG, "launchTaskAnimated - launchDesktopFromRecents: ${taskIds.contentToString()}") + + if (taskIdToReorderToFront != null) { + // The to-be-activated window should animate on top of other apps during shell + // transition. + val remoteTargetHandle = getRemoteTargetHandle(taskIdToReorderToFront) + // The layer swapping is only applied after [createRecentsWindowAnimator] starts, which + // will bring the [remoteTargetHandles] above Recents, therefore this call won't affect + // the base surface in [DepthController]. + remoteTargetHandle?.taskViewSimulator?.setDrawsAboveOtherApps(true) + } + val launchDesktopFromRecents = { + desktopController.launchDesktopFromRecents(this, animated, taskIdToReorderToFront) { + endCallback.executeAllAndDestroy() + } + } + if (enableMultipleDesktops(context) && desktopTask?.tasks?.isEmpty() == true) { + recentsView.switchToScreenshot { + recentsView.finishRecentsAnimation( + /* toRecents= */ true, + /* shouldPip= */ false, + launchDesktopFromRecents, + ) + } + } else { + launchDesktopFromRecents() + } + Log.d( + TAG, + "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated", + ) // Callbacks get run from recentsView for case when recents animation already running recentsView.addSideTaskLaunchCallback(endCallback) return endCallback } - override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { - launchTasks() - callback(true) - } + override fun launchAsStaticTile() = launchTaskWithDesktopController(animated = true) - // Desktop tile can't be in split screen - override fun confirmSecondSplitSelectApp(): Boolean = false + override fun launchWithoutAnimation( + isQuickSwitch: Boolean, + callback: (launched: Boolean) -> Unit, + ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false) + + // Return true when Task cannot be launched as fullscreen (i.e. in split select state) to skip + // putting DesktopTaskView to split as it's not supported. + override fun confirmSecondSplitSelectApp(): Boolean = + recentsView?.canLaunchFullscreenTask() != true // TODO(b/330685808) support overlay for Screenshot action override fun setOverlayEnabled(overlayEnabled: Boolean) {} override fun onFullscreenProgressChanged(fullscreenProgress: Float) { - // Don't show background while we are transitioning to/from fullscreen - backgroundView.visibility = if (fullscreenProgress > 0) INVISIBLE else VISIBLE + backgroundView.alpha = 1 - fullscreenProgress + updateSettledProgressFullscreen(fullscreenProgress) } - override fun updateCurrentFullscreenParams() { - super.updateCurrentFullscreenParams() - updateFullscreenParams(snapshotDrawParams) + override fun updateFullscreenParams() { + super.updateFullscreenParams() + updateFullscreenParams(contentViewFullscreenParams) + contentView.cornerRadius = contentViewFullscreenParams.currentCornerRadius } - override fun getThumbnailFullscreenParams() = snapshotDrawParams + override fun addChildrenForAccessibility(outChildren: ArrayList) { + super.addChildrenForAccessibility(outChildren) + ViewUtils.addAccessibleChildToList(backgroundView, outChildren) + } + + fun removeTaskFromExplodedView(taskId: Int, animate: Boolean) { + if (!enableDesktopExplodedView()) { + Log.e( + TAG, + "removeTaskFromExplodedView called when enableDesktopExplodedView flag is false", + ) + return + } + + // Remove the task's [taskContainer] and its associated Views. + val taskContainer = getTaskContainerById(taskId) ?: return + removeAndRecycleThumbnailView(taskContainer) + taskContainer.destroy() + taskContainers = taskContainers.filterNot { it == taskContainer } + + // Dismiss the current DesktopTaskView if all its windows are closed. + if (taskContainers.isEmpty()) { + recentsView?.dismissTaskView(this, animate, /* removeTask= */ true) + } else { + // If this task has a live window, then hide it. + // TODO(b/413120214) The dismissed view should fade out. + getRemoteTargetHandle(taskId)?.let { + it.taskViewSimulator.setTaskRectTransform(Matrix().apply { postScale(0.0f, 0.0f) }) + it.taskViewSimulator.apply(it.transformParams) + } + + // TODO(b/413130378) Nicer handling of multiple quick task dismissals. + taskRemoveAnimator?.cancel() + taskRemoveAnimator = + ObjectAnimator.ofFloat(this, TASK_REMOVE_PROGRESS, 0f, 1f).apply { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + previousOrganizedDesktopTaskPositions = null + taskRemoveAnimator = null + } + } + ) + start() + } + + // Store the current organized positions before computing new ones. This allows us to + // animate from the current layout to the new. + previousOrganizedDesktopTaskPositions = viewModel!!.organizedDesktopTaskPositions + updateTaskPositions() + } + } + + private fun removeAndRecycleThumbnailView(taskContainer: TaskContainer) { + contentView.removeView(taskContainer.taskContentView) + when { + enableRefactorTaskContentView() -> + taskContentViewPool!!.recycle(taskContainer.taskContentView as TaskContentView) + enableRefactorTaskThumbnail() -> + taskThumbnailViewPool!!.recycle(taskContainer.taskContentView as TaskThumbnailView) + else -> taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated) + } + } + + private fun updateTaskPositions() { + BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF) + val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt()) + DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4) + + fullscreenTaskPositions = + taskContainers.map { + DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS) + } + + if (enableDesktopExplodedView()) { + val (widthScale, heightScale) = getScreenScaleFactors() + val res = context.resources + val layoutConfig = + DesktopLayoutConfig( + topBottomMarginOneRow = + (res.getDimensionPixelSize(R.dimen.desktop_top_bottom_margin_one_row) / + heightScale) + .toInt(), + topMarginMultiRows = + (res.getDimensionPixelSize(R.dimen.desktop_top_margin_multi_rows) / + heightScale) + .toInt(), + bottomMarginMultiRows = + (res.getDimensionPixelSize(R.dimen.desktop_bottom_margin_multi_rows) / + heightScale) + .toInt(), + leftRightMarginOneRow = + (res.getDimensionPixelSize(R.dimen.desktop_left_right_margin_one_row) / + widthScale) + .toInt(), + leftRightMarginMultiRows = + (res.getDimensionPixelSize(R.dimen.desktop_left_right_margin_multi_rows) / + widthScale) + .toInt(), + horizontalPaddingBetweenTasks = + (res.getDimensionPixelSize( + R.dimen.desktop_horizontal_padding_between_tasks + ) / widthScale) + .toInt(), + verticalPaddingBetweenTasks = + (res.getDimensionPixelSize(R.dimen.desktop_vertical_padding_between_tasks) / + heightScale) + .toInt(), + ) + + viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions, layoutConfig) + } + positionTaskWindows(updateLayout = true) + } + + /** + * Calculates the scale factors for the desktop task view's width and height. This is determined + * by comparing the available task view dimensions (after accounting for margins like + * [thumbnailTopMarginPx]) against the total screen dimensions. + * + * @return A [Pair] where the first value is the scale factor for width and the second is for + * height. + */ + private fun getScreenScaleFactors(): Pair { + val thumbnailTopMarginPx = container.deviceProfile.overviewProfile.taskThumbnailTopMarginPx + val taskViewWidth = layoutParams.width + val taskViewHeight = layoutParams.height - thumbnailTopMarginPx + + val screenRect = getScreenRect() + val widthScale = taskViewWidth / screenRect.width().toFloat() + val heightScale = taskViewHeight / screenRect.height().toFloat() + + return Pair(widthScale, heightScale) + } + + /** Returns the dimensions of the screen. */ + private fun getScreenRect(): Rect { + BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF) + return Rect(0, 0, tempPointF.x.toInt(), tempPointF.y.toInt()) + } companion object { private const val TAG = "DesktopTaskView" private const val DEBUG = false - private const val VIEW_POOL_MAX_SIZE = 10 + private const val VIEW_POOL_MAX_SIZE = 5 + // As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool. private const val VIEW_POOL_INITIAL_SIZE = 0 - private val ORIGIN = Point(0, 0) + private val DEFAULT_BOUNDS = Rect() + // Temporaries used for various purposes to avoid allocations. + private val TEMP_OVERVIEW_TASK_POSITION = Rect() + private val TEMP_FROM_RECTF = RectF() + private val TEMP_TO_RECTF = RectF() + private val TASK_REMOVE_PROGRESS: FloatProperty = + KFloatProperty(DesktopTaskView::taskRemoveProgress) } } diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt new file mode 100644 index 0000000000..8c7f4830db --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views + +import android.annotation.SuppressLint +import android.app.ActivityOptions +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.AppUsageLimit +import android.graphics.Outline +import android.graphics.Paint +import android.os.UserHandle +import android.provider.Settings +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.view.ViewOutlineProvider +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.core.util.component1 +import androidx.core.util.component2 +import androidx.core.view.isVisible +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.util.Executors +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED +import com.android.launcher3.util.SplitConfigurationOptions.StagePosition +import com.android.quickstep.TaskUtils +import com.android.quickstep.task.apptimer.DurationFormatter +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.split.SplitBounds +import java.time.Duration + +@SuppressLint("AppCompatCustomView") +class DigitalWellBeingToast +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : TextView(context, attrs, defStyleAttr, defStyleRes) { + private val recentsViewContainer: RecentsViewContainer = + RecentsViewContainer.containerFromContext(context) + + private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java) + + private val bannerHeight = + context.resources.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height) + + private lateinit var task: Task + private lateinit var taskView: TaskView + private lateinit var snapshotView: View + @StagePosition private var stagePosition = STAGE_POSITION_UNDEFINED + + private var appRemainingTimeMs: Long = 0 + private var splitOffsetTranslationY = 0f + set(value) { + if (field != value) { + field = value + updateTranslationY() + } + } + + var isDestroyed = false + private set + + var hasLimit = false + var splitBounds: SplitBounds? = null + var bannerOffsetPercentage = 0f + set(value) { + if (field != value) { + field = value + updateTranslationY() + } + } + + init { + setOnClickListener(::openAppUsageSettings) + outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + BACKGROUND.getOutline(view, outline) + val verticalTranslation = splitOffsetTranslationY - translationY + outline.offset(0, Math.round(verticalTranslation)) + } + } + clipToOutline = true + } + + private fun setNoLimit() { + isVisible = false + hasLimit = false + appRemainingTimeMs = -1 + setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1) + } + + private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) { + isVisible = true + hasLimit = true + this.appRemainingTimeMs = appRemainingTimeMs + setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs) + text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText()) + } + + private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) { + val contentDescription = + getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs) + snapshotView.contentDescription = contentDescription + } + + fun initialize() { + check(!isDestroyed) { "Cannot re-initialize a destroyed toast" } + setupTranslations() + Executors.ORDERED_BG_EXECUTOR.execute { + var usageLimit: AppUsageLimit? = null + try { + usageLimit = + launcherApps?.getAppUsageLimit( + task.topComponent.packageName, + UserHandle.of(task.key.userId), + ) + } catch (e: Exception) { + Log.e(TAG, "Error initializing digital well being toast", e) + } + val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1 + val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1 + + taskView.post { + if (isDestroyed) return@post + if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) { + setNoLimit() + } else { + setLimit(appUsageLimitTimeMs, appRemainingTimeMs) + } + } + } + } + + /** Bind the DWB toast to its dependencies. */ + fun bind( + task: Task, + taskView: TaskView, + snapshotView: View, + @StagePosition stagePosition: Int, + ) { + this.task = task + this.taskView = taskView + this.snapshotView = snapshotView + this.stagePosition = stagePosition + isDestroyed = false + } + + /** Mark the DWB toast as destroyed and hide it. */ + fun destroy() { + isVisible = false + isDestroyed = true + } + + private fun getSplitBannerConfig(): SplitBannerConfig { + val splitBounds = splitBounds + return when { + splitBounds == null || + !recentsViewContainer.deviceProfile.deviceProperties.isTablet || + taskView.isLargeTile -> SplitBannerConfig.SPLIT_BANNER_FULLSCREEN + // For portrait grid only height of task changes, not width. So we keep the text the + // same + !recentsViewContainer.deviceProfile.isLeftRightSplit -> + SplitBannerConfig.SPLIT_GRID_BANNER_LARGE + // For landscape grid, for 30% width we only show icon, otherwise show icon and time + task.key.id == splitBounds.leftTopTaskId -> + if (splitBounds.leftTopTaskPercent < THRESHOLD_LEFT_ICON_ONLY) + SplitBannerConfig.SPLIT_GRID_BANNER_SMALL + else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE + else -> + if (splitBounds.leftTopTaskPercent > THRESHOLD_RIGHT_ICON_ONLY) + SplitBannerConfig.SPLIT_GRID_BANNER_SMALL + else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE + } + } + + /** + * Returns text to show for the banner depending on [.getSplitBannerConfig] If {@param + * forContentDesc} is `true`, this will always return the full string corresponding to + * [.SPLIT_BANNER_FULLSCREEN] + */ + @JvmOverloads + @VisibleForTesting + fun getBannerText( + remainingTime: Long = appRemainingTimeMs, + forContentDesc: Boolean = false, + ): String { + val duration = + Duration.ofMillis( + if (remainingTime > MINUTE_MS) + (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS + else remainingTime + ) + val readableDuration = + DurationFormatter.format( + context, + duration, + R.string.shorter_duration_less_than_one_minute, /* forceFormatWidth */ + ) + val splitBannerConfig = getSplitBannerConfig() + return when { + forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN -> + context.getString(R.string.time_left_for_app, readableDuration) + // show no text + splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> "" + // SPLIT_GRID_BANNER_LARGE only show time + else -> readableDuration + } + } + + private fun openAppUsageSettings(view: View) { + val intent = + Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE) + .putExtra(Intent.EXTRA_PACKAGE_NAME, task.topComponent.packageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + try { + val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height) + context.startActivity(intent, options.toBundle()) + + // TODO: add WW logging on the app usage settings click. + } catch (e: ActivityNotFoundException) { + Log.e( + TAG, + "Failed to open app usage settings for task " + task.topComponent.packageName, + e, + ) + } + } + + private fun getContentDescriptionForTask( + task: Task, + appUsageLimitTimeMs: Long, + appRemainingTimeMs: Long, + ): String? = + if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0) + context.getString( + R.string.task_contents_description_with_remaining_time, + task.titleDescription, + getBannerText(appRemainingTimeMs, true /* forContentDesc */), + ) + else task.titleDescription + + fun setupLayout() { + val snapshotWidth: Int + val snapshotHeight: Int + val splitBounds = splitBounds + if (splitBounds == null) { + snapshotWidth = taskView.layoutParams.width + snapshotHeight = + taskView.layoutParams.height - + recentsViewContainer.deviceProfile.overviewProfile.taskThumbnailTopMarginPx + } else { + val groupedTaskSize = + taskView.pagedOrientationHandler.getGroupedTaskViewSizes( + recentsViewContainer.deviceProfile, + splitBounds, + taskView.layoutParams.width, + taskView.layoutParams.height, + ) + if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) { + snapshotWidth = groupedTaskSize.first.x + snapshotHeight = groupedTaskSize.first.y + } else { + snapshotWidth = groupedTaskSize.second.x + snapshotHeight = groupedTaskSize.second.y + } + } + taskView.pagedOrientationHandler.updateDwbBannerLayout( + taskView.layoutParams.width, + taskView.layoutParams.height, + taskView is GroupedTaskView, + recentsViewContainer.deviceProfile, + snapshotWidth, + snapshotHeight, + this, + ) + } + + private fun setupTranslations() { + val (translationX, translationY) = + taskView.pagedOrientationHandler.getDwbBannerTranslations( + taskView.layoutParams.width, + taskView.layoutParams.height, + splitBounds, + recentsViewContainer.deviceProfile, + taskView.taskContentViews, + task.key.id, + this, + ) + this.translationX = translationX + this.splitOffsetTranslationY = translationY + } + + private fun updateTranslationY() { + translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY + invalidateOutline() + } + + fun setColorTint(color: Int, amount: Float) { + if (amount == 0f) { + setLayerType(View.LAYER_TYPE_NONE, null) + } + val layerPaint = Paint() + layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount)) + setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint) + setLayerPaint(layerPaint) + } + + private fun getAccessibilityActionId(): Int = + if (splitBounds?.rightBottomTaskId == task.key.id) + R.id.action_digital_wellbeing_bottom_right + else R.id.action_digital_wellbeing_top_left + + fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? { + if (!hasLimit) return null + val label = + if (taskView.containsMultipleTasks()) + context.getString( + R.string.split_app_usage_settings, + TaskUtils.getTitle(context, task), + ) + else context.getString(R.string.accessibility_app_usage_settings) + return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label) + } + + fun handleAccessibilityAction(action: Int): Boolean { + if (getAccessibilityActionId() != action) return false + openAppUsageSettings(taskView) + return true + } + + companion object { + private const val THRESHOLD_LEFT_ICON_ONLY = 0.4f + private const val THRESHOLD_RIGHT_ICON_ONLY = 0.6f + + enum class SplitBannerConfig { + /** Will span entire width of taskView with full text */ + SPLIT_BANNER_FULLSCREEN, + /** Used for grid task view, only showing icon and time */ + SPLIT_GRID_BANNER_LARGE, + /** Used for grid task view, only showing icon */ + SPLIT_GRID_BANNER_SMALL, + } + + val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS) + const val MINUTE_MS: Int = 60000 + + private const val TAG = "DigitalWellBeingToast" + } +} diff --git a/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt new file mode 100644 index 0000000000..c8930165b5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/FixedSizeImageView.kt @@ -0,0 +1,56 @@ +/* + * 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.quickstep.views + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.ImageView + +/** + * An [ImageView] that does not requestLayout() unless setLayoutParams is called. + * + * This is useful, particularly during animations, for [ImageView]s that are not supposed to be + * resized. + */ +@SuppressLint("AppCompatCustomView") +class FixedSizeImageView : ImageView { + private var shouldRequestLayoutOnChanges = false + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + override fun setLayoutParams(params: ViewGroup.LayoutParams?) { + shouldRequestLayoutOnChanges = true + super.setLayoutParams(params) + shouldRequestLayoutOnChanges = false + } + + override fun requestLayout() { + if (shouldRequestLayoutOnChanges) { + super.requestLayout() + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt index e024995aa3..4361a33aa5 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt +++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt @@ -37,12 +37,12 @@ import com.android.systemui.shared.system.QuickStepContract * increase in size during the animation. */ open class FloatingAppPairBackground( - context: Context, - // the view that we will draw this background on - protected val floatingView: FloatingAppPairView, - private val appIcon1: Drawable, - private val appIcon2: Drawable?, - dividerPos: Int + context: Context, + // the view that we will draw this background on + protected val floatingView: FloatingAppPairView, + private val appIcon1: Drawable, + private val appIcon2: Drawable?, + dividerPos: Int, ) : Drawable() { companion object { // Design specs -- app icons start small and expand during the animation @@ -57,6 +57,7 @@ open class FloatingAppPairBackground( private val container: RecentsViewContainer private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG) // Animation interpolators protected val expandXInterpolator: Interpolator @@ -78,40 +79,45 @@ open class FloatingAppPairBackground( backgroundPaint.color = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0) ta.recycle() // Set up timings and interpolators - val timings = AnimUtils.getDeviceAppPairLaunchTimings(container.deviceProfile.isTablet) + val timings = + AnimUtils.getDeviceAppPairLaunchTimings( + container.deviceProfile.deviceProperties.isTablet + ) expandXInterpolator = Interpolators.clampToProgress( timings.getStagedRectScaleXInterpolator(), timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset + timings.stagedRectSlideEndOffset, ) expandYInterpolator = Interpolators.clampToProgress( timings.getStagedRectScaleYInterpolator(), timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset + timings.stagedRectSlideEndOffset, ) cellSplitInterpolator = Interpolators.clampToProgress( timings.cellSplitInterpolator, timings.cellSplitStartOffset, - timings.cellSplitEndOffset + timings.cellSplitEndOffset, ) iconFadeInterpolator = Interpolators.clampToProgress( timings.iconFadeInterpolator, timings.iconFadeStartOffset, - timings.iconFadeEndOffset + timings.iconFadeEndOffset, ) // Find device-specific measurements - deviceCornerRadius = QuickStepContract.getWindowCornerRadius(container.asContext()) + val resources = context.resources + deviceCornerRadius = QuickStepContract.getWindowCornerRadius(context) deviceHalfDividerSize = - container.asContext().resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f + resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f val dividerCenterPos = dividerPos + deviceHalfDividerSize desiredSplitRatio = - if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx - else dividerCenterPos / dp.heightPx + if (dp.isLeftRightSplit) dividerCenterPos / dp.deviceProperties.widthPx + else dividerCenterPos / dp.deviceProperties.heightPx + dividerPaint.color = resources.getColor(R.color.taskbar_background_dark, null /*theme*/) } override fun draw(canvas: Canvas) { @@ -153,8 +159,17 @@ open class FloatingAppPairBackground( val leftSide = RectF(0f, 0f, dividerCenterPos - changingDividerSize, height) // The right half of the background image val rightSide = RectF(dividerCenterPos + changingDividerSize, 0f, width, height) + // Middle part is for divider background + val middleRect = + RectF( + leftSide.right - deviceHalfDividerSize, + 0f, + rightSide.left + deviceHalfDividerSize, + height, + ) // Draw background + canvas.drawRect(middleRect, dividerPaint) drawCustomRoundedRect( canvas, leftSide, @@ -167,7 +182,7 @@ open class FloatingAppPairBackground( changingInnerRadiusY, cornerRadiusX, cornerRadiusY, - ) + ), ) drawCustomRoundedRect( canvas, @@ -181,7 +196,7 @@ open class FloatingAppPairBackground( cornerRadiusY, changingInnerRadiusX, changingInnerRadiusY, - ) + ), ) // Calculate changing measurements for icons. @@ -251,8 +266,17 @@ open class FloatingAppPairBackground( val topSide = RectF(0f, 0f, width, dividerCenterPos - changingDividerSize) // The bottom half of the background image val bottomSide = RectF(0f, dividerCenterPos + changingDividerSize, width, height) + // Middle part is for divider background + val middleRect = + RectF( + 0f, + topSide.bottom - deviceHalfDividerSize, + width, + bottomSide.top + deviceHalfDividerSize, + ) // Draw background + canvas.drawRect(middleRect, dividerPaint) drawCustomRoundedRect( canvas, topSide, @@ -264,8 +288,8 @@ open class FloatingAppPairBackground( changingInnerRadiusX, changingInnerRadiusY, changingInnerRadiusX, - changingInnerRadiusY - ) + changingInnerRadiusY, + ), ) drawCustomRoundedRect( canvas, @@ -278,8 +302,8 @@ open class FloatingAppPairBackground( cornerRadiusX, cornerRadiusY, cornerRadiusX, - cornerRadiusY - ) + cornerRadiusY, + ), ) // Calculate changing measurements for icons. diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt index e8d1cc1e36..668413817a 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt +++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt @@ -27,7 +27,7 @@ import android.view.ViewGroup import android.widget.FrameLayout import com.android.launcher3.R import com.android.launcher3.Utilities -import com.android.launcher3.statemanager.StatefulActivity +import com.android.launcher3.views.ActivityContext import com.android.launcher3.views.BaseDragLayer /** @@ -38,11 +38,11 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att FrameLayout(context, attrs) { companion object { fun getFloatingAppPairView( - launcher: StatefulActivity<*>, + launcher: ActivityContext, originalView: View, appIcon1: Drawable?, appIcon2: Drawable?, - dividerPos: Int + dividerPos: Int, ): FloatingAppPairView { val dragLayer: ViewGroup = launcher.getDragLayer() val floatingView = @@ -62,11 +62,11 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att /** Initializes the view, copying the bounds and location of the original icon view. */ fun init( - launcher: StatefulActivity<*>, + launcher: ActivityContext, originalView: View, appIcon1: Drawable?, appIcon2: Drawable?, - dividerPos: Int + dividerPos: Int, ) { val viewBounds = Rect(0, 0, originalView.width, originalView.height) Utilities.getBoundsForViewInDragLayer( @@ -75,12 +75,12 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att viewBounds, false /* ignoreTransform */, null /* recycle */, - startingPosition + startingPosition, ) val lp = BaseDragLayer.LayoutParams( Math.round(startingPosition.width()), - Math.round(startingPosition.height()) + Math.round(startingPosition.height()), ) lp.ignoreInsets = true @@ -92,14 +92,14 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att layoutParams = lp // Prepare to draw app pair icon background - background = if (appIcon1 == null || appIcon2 == null) { - val iconToAnimate = appIcon1 ?: appIcon2 - checkNotNull(iconToAnimate) - FloatingFullscreenAppPairBackground(context, this, iconToAnimate, - dividerPos) - } else { - FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos) - } + background = + if (appIcon1 == null || appIcon2 == null) { + val iconToAnimate = appIcon1 ?: appIcon2 + checkNotNull(iconToAnimate) + FloatingFullscreenAppPairBackground(context, this, iconToAnimate, dividerPos) + } else { + FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos) + } background.setBounds(0, 0, lp.width, lp.height) } diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java index acbb2eccbb..72a823efe1 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java @@ -239,7 +239,7 @@ public class FloatingTaskView extends FrameLayout { // Position the floating view exactly on top of the original lp.topMargin = Math.round(pos.top); if (mIsRtl) { - lp.setMarginStart(mContainer.getDeviceProfile().widthPx - Math.round(pos.right)); + lp.setMarginStart(mContainer.getDeviceProfile().getDeviceProperties().getWidthPx() - Math.round(pos.right)); } else { lp.setMarginStart(Math.round(pos.left)); } @@ -256,7 +256,7 @@ public class FloatingTaskView extends FrameLayout { */ public void addStagingAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) { - boolean isTablet = mContainer.getDeviceProfile().isTablet; + boolean isTablet = mContainer.getDeviceProfile().getDeviceProperties().isTablet(); boolean splittingFromOverview = fadeWithThumbnail; SplitAnimationTimings timings; @@ -280,7 +280,7 @@ public class FloatingTaskView extends FrameLayout { public void addConfirmAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) { SplitAnimationTimings timings = - AnimUtils.getDeviceSplitToConfirmTimings(mContainer.getDeviceProfile().isTablet); + AnimUtils.getDeviceSplitToConfirmTimings(mContainer.getDeviceProfile().getDeviceProperties().isTablet()); addAnimation(animation, startingBounds, endBounds, fadeWithThumbnail, isStagedTask, timings); diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java index e5f241fdbe..b060168b1b 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java @@ -33,7 +33,6 @@ import androidx.annotation.Nullable; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.widget.LauncherAppWidgetHostView; -import com.android.launcher3.widget.RoundedCornerEnforcement; import java.util.stream.IntStream; @@ -180,8 +179,7 @@ final class FloatingWidgetBackgroundView extends View { /** Corner radius from source view's outline, or enforced view. */ private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) { - if (RoundedCornerEnforcement.isRoundedCornerEnabled() - && hostView.hasEnforcedCornerRadius()) { + if (hostView.hasEnforcedCornerRadius()) { return hostView.getEnforcedCornerRadius(); } else if (Utilities.ATLEAST_S && v.getOutlineProvider() instanceof RemoteViewOutlineProvider diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java index fc52b8ee93..7bddc23231 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java @@ -18,6 +18,7 @@ package com.android.quickstep.views; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.annotation.TargetApi; +import android.app.TaskInfo; import android.content.Context; import android.graphics.Matrix; import android.graphics.RectF; @@ -49,7 +50,6 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, OnGlobalLayoutListener, FloatingView { private static final Matrix sTmpMatrix = new Matrix(); - private final QuickstepLauncher mLauncher; private final ListenerView mListenerView; private final FloatingWidgetBackgroundView mBackgroundView; private final RectF mBackgroundOffset = new RectF(); @@ -80,7 +80,6 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, public FloatingWidgetView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mLauncher = QuickstepLauncher.getLauncher(context); mListenerView = new ListenerView(context, attrs); mBackgroundView = new FloatingWidgetBackgroundView(context, attrs, defStyleAttr); addView(mBackgroundView); @@ -122,10 +121,9 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, @Override public void onGlobalLayout() { - if (isUninitialized()) - return; - positionViews(); - if (mOnTargetChangeRunnable != null) { + if (isUninitialized()) return; + boolean positionsChanged = positionViews(); + if (mOnTargetChangeRunnable != null && positionsChanged) { mOnTargetChangeRunnable.run(); } } @@ -143,8 +141,7 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, /** Callback at the end or early exit of the animation. */ @Override public void fastFinish() { - if (isUninitialized()) - return; + if (isUninitialized()) return; Runnable fastFinishRunnable = mFastFinishRunnable; if (fastFinishRunnable != null) { fastFinishRunnable.run(); @@ -156,7 +153,8 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, } } - private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView, + private void init(QuickstepLauncher launcher, DragLayer dragLayer, + LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius, boolean appTargetIsTranslucent, int fallbackBackgroundColor) { mAppWidgetView = originalView; @@ -164,7 +162,7 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, mAppWidgetView.beginDeferringUpdates(); mBackgroundPosition = widgetBackgroundPosition; mAppTargetIsTranslucent = appTargetIsTranslucent; - mEndRunnable = () -> finish(dragLayer); + mEndRunnable = () -> finish(launcher, dragLayer); mAppWidgetBackgroundView = RoundedCornerEnforcement.findBackground(mAppWidgetView); if (mAppWidgetBackgroundView == null) { @@ -189,23 +187,18 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, /** * Updates the position and opacity of the floating widget's components. * - * @param backgroundPosition the new position of the widget's background - * relative to the + * @param backgroundPosition the new position of the widget's background relative to the * {@link FloatingWidgetView}'s parent - * @param floatingWidgetAlpha the overall opacity of the - * {@link FloatingWidgetView} + * @param floatingWidgetAlpha the overall opacity of the {@link FloatingWidgetView} * @param foregroundAlpha the opacity of the foreground layer - * @param fallbackBackgroundAlpha the opacity of the fallback background used - * when the App + * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App * Widget doesn't have a background - * @param cornerRadiusProgress progress of the corner radius animation, where - * 0 is the + * @param cornerRadiusProgress progress of the corner radius animation, where 0 is the * original radius and 1 is the window radius */ public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha, float fallbackBackgroundAlpha, float cornerRadiusProgress) { - if (isUninitialized() || mAppTargetIsTranslucent) - return; + if (isUninitialized() || mAppTargetIsTranslucent) return; setAlpha(floatingWidgetAlpha); mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha); mAppWidgetView.setAlpha(foregroundAlpha); @@ -220,37 +213,64 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, } /** - * Sets the layout parameters of the floating view and its background view - * child. + * Sets the layout parameters of the floating view and its background view child. + * @return true if any of the views positions change due to this call. */ - private void positionViews() { + private boolean positionViews() { + boolean positionsChanged = false; + LayoutParams layoutParams = (LayoutParams) getLayoutParams(); - layoutParams.setMargins(0, 0, 0, 0); - setLayoutParams(layoutParams); + + if (layoutParams.topMargin != 0 || layoutParams.bottomMargin != 0 + || layoutParams.rightMargin != 0 || layoutParams.leftMargin != 0) { + positionsChanged = true; + layoutParams.setMargins(0, 0, 0, 0); + setLayoutParams(layoutParams); + } // FloatingWidgetView layout is forced LTR - mBackgroundView.setTranslationX(mBackgroundPosition.left); - mBackgroundView.setTranslationY(mBackgroundPosition.top + mIconOffsetY); + float targetY = mBackgroundPosition.top + mIconOffsetY; + if (mBackgroundView.getTranslationX() != mBackgroundPosition.left + || mBackgroundView.getTranslationY() != targetY) { + positionsChanged = true; + mBackgroundView.setTranslationX(mBackgroundPosition.left); + mBackgroundView.setTranslationY(targetY); + } + LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams(); - backgroundParams.leftMargin = 0; - backgroundParams.topMargin = 0; - backgroundParams.width = (int) mBackgroundPosition.width(); - backgroundParams.height = (int) mBackgroundPosition.height(); - mBackgroundView.setLayoutParams(backgroundParams); + if (backgroundParams.leftMargin != 0 || backgroundParams.topMargin != 0 + || backgroundParams.width != Math.round(mBackgroundPosition.width()) + || backgroundParams.height != Math.round(mBackgroundPosition.height())) { + positionsChanged = true; + + backgroundParams.leftMargin = 0; + backgroundParams.topMargin = 0; + backgroundParams.width = Math.round(mBackgroundPosition.width()); + backgroundParams.height = Math.round(mBackgroundPosition.height()); + mBackgroundView.setLayoutParams(backgroundParams); + } if (mForegroundOverlayView != null) { sTmpMatrix.reset(); - float foregroundScale = mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth(); + float foregroundScale = + mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth(); sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(), -mBackgroundOffset.top - mAppWidgetView.getTop()); sTmpMatrix.postScale(foregroundScale, foregroundScale); sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top + mIconOffsetY); - mForegroundOverlayView.setMatrix(sTmpMatrix); + + // We use the animation matrix here, because calling setMatrix on the GhostView + // actually sets the animation matrix, not the regular one. + if (!sTmpMatrix.equals(mForegroundOverlayView.getAnimationMatrix())) { + positionsChanged = true; + mForegroundOverlayView.setMatrix(sTmpMatrix); + } } + return positionsChanged; } - private void finish(DragLayer dragLayer) { + private void finish(QuickstepLauncher launcher, DragLayer dragLayer) { mAppWidgetView.setAlpha(1f); GhostView.removeGhost(mAppWidgetView); ((ViewGroup) dragLayer.getParent()).removeView(this); @@ -259,7 +279,7 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, // Removing GhostView must occur before ending deferrals. See b/190818220 mAppWidgetView.endDeferringUpdates(); recycle(); - mLauncher.getViewCache().recycleView(R.layout.floating_widget_view, this); + launcher.getViewCache().recycleView(R.layout.floating_widget_view, this); } public float getInitialCornerRadius() { @@ -284,12 +304,10 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, } /** - * Configures and returns a an instance of {@link FloatingWidgetView} matching - * the appearance of + * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of * {@param originalView}. * - * @param widgetBackgroundPosition a {@link RectF} that will be updated with the - * widget's + * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's * background bounds * @param windowSize the size of the window when launched * @param windowCornerRadius the corner radius of the window @@ -300,31 +318,35 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener, int fallbackBackgroundColor) { final DragLayer dragLayer = launcher.getDragLayer(); ViewGroup parent = (ViewGroup) dragLayer.getParent(); - FloatingWidgetView floatingView = launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, - parent); + FloatingWidgetView floatingView = + launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent); floatingView.recycle(); - floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowSize, + floatingView.init(launcher, dragLayer, originalView, widgetBackgroundPosition, windowSize, windowCornerRadius, appTargetsAreTranslucent, fallbackBackgroundColor); parent.addView(floatingView); return floatingView; } /** - * Extract a background color from a target's task description, or fall back to - * the given + * Extract a background color from a target's task description, or fall back to the given * context's theme background color. */ public static int getDefaultBackgroundColor( - Context context, RemoteAnimationTarget target) { - return (target != null && target.taskInfo != null - && target.taskInfo.taskDescription != null) - ? target.taskInfo.taskDescription.getBackgroundColor() - : Themes.getColorBackground(context); + Context context, @Nullable RemoteAnimationTarget target) { + final int fallbackColor = Themes.getColorBackground(context); + if (target == null) { + return fallbackColor; + } + final TaskInfo taskInfo = target.taskInfo; + if (taskInfo == null) { + return fallbackColor; + } + return taskInfo.taskDescription.getBackgroundColor(); } private static void getRelativePosition(View descendant, View ancestor, RectF position) { - float[] points = new float[] { 0, 0, descendant.getWidth(), descendant.getHeight() }; + float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()}; Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points, false /* includeRootScroll */, true /* ignoreTransform */); position.set( diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt index d6a3376c53..7a8a6c59fd 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt @@ -21,24 +21,26 @@ import android.graphics.PointF import android.util.AttributeSet import android.util.Log import android.view.View +import android.view.ViewStub import com.android.internal.jank.Cuj -import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorDigitalWellbeingToast +import com.android.launcher3.Flags.enableRefactorTaskContentView +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.R import com.android.launcher3.Utilities -import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu import com.android.launcher3.util.RunnableList -import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED import com.android.quickstep.TaskOverlayFactory import com.android.quickstep.util.RecentsOrientedState -import com.android.quickstep.util.SplitScreenUtils.Companion.convertLauncherSplitBoundsToShell import com.android.quickstep.util.SplitSelectStateController -import com.android.systemui.shared.recents.model.Task -import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.android.quickstep.util.SplitTask import com.android.systemui.shared.system.InteractionJankMonitorWrapper -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition +import com.android.wm.shell.Flags.enableFlexibleTwoAppSplit +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition /** * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks @@ -51,9 +53,18 @@ import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosi * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included). */ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - TaskView(context, attrs) { + TaskView(context, attrs, type = TaskViewType.GROUPED) { + + private val MINIMUM_RATIO_TO_SHOW_ICON = 0.25f + + val leftTopTaskContainer: TaskContainer + get() = taskContainers[0] + + val rightBottomTaskContainer: TaskContainer + get() = taskContainers[1] + // TODO(b/336612373): Support new TTV for GroupedTaskView - var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null + var splitBoundsConfig: SplitBounds? = null private set @get:PersistentSnapPosition @@ -67,103 +78,91 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val heightSize = MeasureSpec.getSize(heightMeasureSpec) setMeasuredDimension(widthSize, heightSize) val splitBoundsConfig = splitBoundsConfig ?: return - val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() - if (initSplitTaskId == INVALID_TASK_ID) { - pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( - taskContainers[0].thumbnailViewDeprecated, - taskContainers[1].thumbnailViewDeprecated, - widthSize, - heightSize, - splitBoundsConfig, - container.deviceProfile, - layoutDirection == LAYOUT_DIRECTION_RTL - ) - // Should we be having a separate translation step apart from the measuring above? - // The following only applies to large screen for now, but for future reference - // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary - // translation directions - taskContainers[0] - .thumbnailViewDeprecated - .applySplitSelectTranslateX(taskContainers[0].thumbnailViewDeprecated.translationX) - taskContainers[0] - .thumbnailViewDeprecated - .applySplitSelectTranslateY(taskContainers[0].thumbnailViewDeprecated.translationY) - taskContainers[1] - .thumbnailViewDeprecated - .applySplitSelectTranslateX(taskContainers[1].thumbnailViewDeprecated.translationX) - taskContainers[1] - .thumbnailViewDeprecated - .applySplitSelectTranslateY(taskContainers[1].thumbnailViewDeprecated.translationY) - } else { - // Currently being split with this taskView, let the non-split selected thumbnail - // take up full thumbnail area - taskContainers - .firstOrNull { it.task.key.id != initSplitTaskId } - ?.thumbnailViewDeprecated - ?.measure( - widthMeasureSpec, - MeasureSpec.makeMeasureSpec( - heightSize - container.deviceProfile.overviewTaskThumbnailTopMarginPx, - MeasureSpec.EXACTLY - ) - ) - } + val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID + pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( + leftTopTaskContainer.taskContentView, + rightBottomTaskContainer.taskContentView, + widthSize, + heightSize, + splitBoundsConfig, + container.deviceProfile, + layoutDirection == LAYOUT_DIRECTION_RTL, + inSplitSelection, + ) + if (!enableOverviewIconMenu()) { updateIconPlacement() } } + override fun inflateViewStubs() { + super.inflateViewStubs() + findViewById(R.id.bottomright_task_content_view) + ?.apply { + inflatedId = + if (enableRefactorTaskContentView()) R.id.bottomright_task_content_view + else R.id.bottomright_snapshot + layoutResource = + when { + enableRefactorTaskContentView() -> R.layout.task_content_view + enableRefactorTaskThumbnail() -> R.layout.task_thumbnail + else -> R.layout.task_thumbnail_deprecated + } + } + ?.inflate() + findViewById(R.id.bottomRight_icon) + ?.apply { + layoutResource = + if (enableOverviewIconMenu()) R.layout.icon_app_chip_view + else R.layout.icon_view + } + ?.inflate() + if (!enableRefactorDigitalWellbeingToast()) { + findViewById(R.id.bottomRight_digital_wellbeing_toast) + ?.apply { layoutResource = R.layout.digital_wellbeing_toast } + ?.inflate() + } + } + override fun onRecycle() { super.onRecycle() splitBoundsConfig = null } fun bind( - primaryTask: Task, - secondaryTask: Task, + splitTask: SplitTask, orientedState: RecentsOrientedState, taskOverlayFactory: TaskOverlayFactory, - splitBoundsConfig: SplitConfigurationOptions.SplitBounds?, ) { + this.groupTask = splitTask cancelPendingLoadTasks() taskContainers = listOf( createTaskContainer( - primaryTask, + splitTask.topLeftTask, + R.id.task_content_view, R.id.snapshot, R.id.icon, R.id.show_windows, + R.id.digital_wellbeing_toast, STAGE_POSITION_TOP_OR_LEFT, - taskOverlayFactory + taskOverlayFactory, ), createTaskContainer( - secondaryTask, - R.id.bottomright_snapshot, + splitTask.bottomRightTask, + R.id.bottomright_task_content_view, + if (enableRefactorTaskContentView()) R.id.snapshot + else R.id.bottomright_snapshot, R.id.bottomRight_icon, R.id.show_windows_right, + R.id.bottomRight_digital_wellbeing_toast, STAGE_POSITION_BOTTOM_OR_RIGHT, - taskOverlayFactory - ) + taskOverlayFactory, + ), ) - this.splitBoundsConfig = - splitBoundsConfig?.also { - taskContainers[0] - .thumbnailViewDeprecated - .previewPositionHelper - .setSplitBounds( - convertLauncherSplitBoundsToShell(it), - PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT - ) - taskContainers[1] - .thumbnailViewDeprecated - .previewPositionHelper - .setSplitBounds( - convertLauncherSplitBoundsToShell(it), - PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT - ) - } - taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) } - setOrientationState(orientedState) + this.splitBoundsConfig = splitTask.splitBounds + taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig } + onBind(orientedState) } override fun setOrientationState(orientationState: RecentsOrientedState) { @@ -174,7 +173,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu container.deviceProfile, it, layoutParams.width, - layoutParams.height + layoutParams.height, ) val iconViewMarginStart = resources.getDimensionPixelSize( @@ -187,12 +186,10 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2 // setMaxWidth() needs to be called before mIconView.setIconOrientation which is // called in the super below. - (taskContainers[0].iconView as IconAppChipView).setMaxWidth( + (leftTopTaskContainer.iconView as IconAppChipView).maxWidth = groupedTaskViewSizes.first.x - iconMargins - ) - (taskContainers[1].iconView as IconAppChipView).setMaxWidth( + (rightBottomTaskContainer.iconView as IconAppChipView).maxWidth = groupedTaskViewSizes.second.x - iconMargins - ) } } super.setOrientationState(orientationState) @@ -201,65 +198,93 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu private fun updateIconPlacement() { val splitBoundsConfig = splitBoundsConfig ?: return - val taskIconHeight = container.deviceProfile.overviewTaskIconSizePx - val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL + val deviceProfile = container.deviceProfile + val taskIconHeight = deviceProfile.overviewProfile.taskIconSizePx + val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID + var oneIconHiddenDueToSmallWidth = false + + if (enableFlexibleTwoAppSplit()) { + // Update values for both icons' setFlexSplitAlpha. Mainly, we want to hide an icon if + // its app tile is too small. But we also have to set the alphas back if we go to + // split selection. + val hideLeftTopIcon: Boolean + val hideRightBottomIcon: Boolean + if (inSplitSelection) { + hideLeftTopIcon = + getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.leftTopTaskId + hideRightBottomIcon = + getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.rightBottomTaskId + } else { + hideLeftTopIcon = splitBoundsConfig.leftTopTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON + hideRightBottomIcon = + splitBoundsConfig.rightBottomTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON + if (hideLeftTopIcon || hideRightBottomIcon) { + oneIconHiddenDueToSmallWidth = true + } + } + + leftTopTaskContainer.iconView.setFlexSplitAlpha(if (hideLeftTopIcon) 0f else 1f) + rightBottomTaskContainer.iconView.setFlexSplitAlpha(if (hideRightBottomIcon) 0f else 1f) + } + if (enableOverviewIconMenu()) { + val isDeviceRtl = Utilities.isRtl(resources) val groupedTaskViewSizes = pagedOrientationHandler.getGroupedTaskViewSizes( - container.deviceProfile, + deviceProfile, splitBoundsConfig, layoutParams.width, - layoutParams.height + layoutParams.height, ) pagedOrientationHandler.setSplitIconParams( - taskContainers[0].iconView.asView(), - taskContainers[1].iconView.asView(), + leftTopTaskContainer.iconView.asView(), + rightBottomTaskContainer.iconView.asView(), taskIconHeight, groupedTaskViewSizes.first.x, groupedTaskViewSizes.first.y, layoutParams.height, layoutParams.width, - isRtl, - container.deviceProfile, - splitBoundsConfig + isDeviceRtl, + deviceProfile, + splitBoundsConfig, + inSplitSelection, + oneIconHiddenDueToSmallWidth, ) } else { pagedOrientationHandler.setSplitIconParams( - taskContainers[0].iconView.asView(), - taskContainers[1].iconView.asView(), + leftTopTaskContainer.iconView.asView(), + rightBottomTaskContainer.iconView.asView(), taskIconHeight, - taskContainers[0].thumbnailViewDeprecated.measuredWidth, - taskContainers[0].thumbnailViewDeprecated.measuredHeight, + leftTopTaskContainer.taskContentView.measuredWidth, + leftTopTaskContainer.taskContentView.measuredHeight, measuredHeight, measuredWidth, - isRtl, - container.deviceProfile, - splitBoundsConfig + isLayoutRtl, + deviceProfile, + splitBoundsConfig, + inSplitSelection, + oneIconHiddenDueToSmallWidth, ) } } - fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) { + fun updateSplitBoundsConfig(splitBounds: SplitBounds?) { splitBoundsConfig = splitBounds taskContainers.forEach { - it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) - it.digitalWellBeingToast?.initialize(it.task) + it.digitalWellBeingToast?.splitBounds = splitBoundsConfig + it.digitalWellBeingToast?.initialize() } invalidate() } - override fun launchTaskAnimated(): RunnableList? { - if (taskContainers.isEmpty()) { - Log.d(TAG, "launchTaskAnimated - task is not bound") - return null - } + override fun launchAsStaticTile(): RunnableList? { val recentsView = recentsView ?: return null val endCallback = RunnableList() // Callbacks run from remote animation when recents animation not currently running InteractionJankMonitorWrapper.begin( this, Cuj.CUJ_SPLIT_SCREEN_ENTER, - "Enter form GroupedTaskView" + "Enter form GroupedTaskView", ) launchTaskInternal(isQuickSwitch = false, launchingExistingTaskView = true) { endCallback.executeAllAndDestroy() @@ -271,8 +296,11 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu return endCallback } - override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { - launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/) + override fun launchWithoutAnimation( + isQuickSwitch: Boolean, + callback: (launched: Boolean) -> Unit, + ) { + launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback) } /** @@ -284,19 +312,22 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu private fun launchTaskInternal( isQuickSwitch: Boolean, launchingExistingTaskView: Boolean, - callback: (launched: Boolean) -> Unit + callback: (launched: Boolean) -> Unit, ) { recentsView?.let { it.splitSelectController.launchExistingSplitPair( if (launchingExistingTaskView) this else null, - taskContainers[0].task.key.id, - taskContainers[1].task.key.id, + leftTopTaskContainer.task.key.id, + rightBottomTaskContainer.task.key.id, STAGE_POSITION_TOP_OR_LEFT, callback, isQuickSwitch, - snapPosition + snapPosition, + ) + Log.d( + TAG, + "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView", ) - Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}") } } @@ -317,14 +348,14 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu // checks below aren't reliable since both of those views may be gone/transformed val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() if (initSplitTaskId != INVALID_TASK_ID) { - return if (initSplitTaskId == taskContainers[0].task.key.id) 1 else 0 + return if (initSplitTaskId == leftTopTaskContainer.task.key.id) 1 else 0 } } // Check which of the two apps was selected if ( - taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) || - taskContainers[1].thumbnailViewDeprecated.containsPoint(lastTouchDownPosition) + rightBottomTaskContainer.iconView.asView().containsPoint(lastTouchDownPosition) || + rightBottomTaskContainer.snapshotView.containsPoint(lastTouchDownPosition) ) { return 1 } @@ -337,14 +368,6 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu return Utilities.pointInView(this, localPos[0], localPos[1], 0f /* slop */) } - override fun setOverlayEnabled(overlayEnabled: Boolean) { - if (FeatureFlags.enableAppPairs()) { - super.setOverlayEnabled(overlayEnabled) - } else { - // Intentional no-op to prevent setting smart actions overlay on thumbnails - } - } - companion object { private const val TAG = "GroupedTaskView" } diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java deleted file mode 100644 index ba42594e99..0000000000 --- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java +++ /dev/null @@ -1,464 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.views; - -import static com.android.app.animation.Interpolators.EMPHASIZED; -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.RectEvaluator; -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Outline; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.ViewOutlineProvider; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.MultiPropertyFactory; -import com.android.launcher3.util.MultiValueAlpha; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.util.RecentsOrientedState; - -/** - * An icon app menu view which can be used in place of an IconView in overview TaskViews. - */ -public class IconAppChipView extends FrameLayout implements TaskViewIcon { - - private static final int MENU_BACKGROUND_REVEAL_DURATION = 417; - private static final int MENU_BACKGROUND_HIDE_DURATION = 333; - - private static final int NUM_ALPHA_CHANNELS = 3; - private static final int INDEX_CONTENT_ALPHA = 0; - private static final int INDEX_COLOR_FILTER_ALPHA = 1; - private static final int INDEX_MODAL_ALPHA = 2; - - private final MultiValueAlpha mMultiValueAlpha; - - private View mMenuAnchorView; - private IconView mIconView; - // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name. - private TextView mIconTextCollapsedView; - private TextView mIconTextExpandedView; - private ImageView mIconArrowView; - private final Rect mBackgroundRelativeLtrLocation = new Rect(); - final RectEvaluator mBackgroundAnimationRectEvaluator = - new RectEvaluator(mBackgroundRelativeLtrLocation); - private final int mCollapsedMenuDefaultWidth; - private final int mExpandedMenuDefaultWidth; - private final int mCollapsedMenuDefaultHeight; - private final int mExpandedMenuDefaultHeight; - private final int mIconMenuMarginTopStart; - private final int mMenuToChipGap; - private final int mBackgroundMarginTopStart; - private final int mAppNameHorizontalMargin; - private final int mIconViewMarginStart; - private final int mAppIconSize; - private final int mArrowSize; - private final int mIconViewDrawableExpandedSize; - private final int mArrowMarginEnd; - private AnimatorSet mAnimator; - - private int mMaxWidth = Integer.MAX_VALUE; - - private static final int INDEX_SPLIT_TRANSLATION = 0; - private static final int INDEX_MENU_TRANSLATION = 1; - private static final int INDEX_COUNT_TRANSLATION = 2; - - private final MultiPropertyFactory mViewTranslationX; - private final MultiPropertyFactory mViewTranslationY; - - /** - * Gets the view split x-axis translation - */ - public MultiPropertyFactory.MultiProperty getSplitTranslationX() { - return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION); - } - - /** - * Sets the view split x-axis translation - * @param translationX x-axis translation - */ - public void setSplitTranslationX(float translationX) { - getSplitTranslationX().setValue(translationX); - } - - /** - * Gets the view split y-axis translation - */ - public MultiPropertyFactory.MultiProperty getSplitTranslationY() { - return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION); - } - - /** - * Sets the view split y-axis translation - * @param translationY y-axis translation - */ - public void setSplitTranslationY(float translationY) { - getSplitTranslationY().setValue(translationY); - } - - /** - * Gets the menu x-axis translation for split task - */ - public MultiPropertyFactory.MultiProperty getMenuTranslationX() { - return mViewTranslationX.get(INDEX_MENU_TRANSLATION); - } - - /** - * Gets the menu y-axis translation for split task - */ - public MultiPropertyFactory.MultiProperty getMenuTranslationY() { - return mViewTranslationY.get(INDEX_MENU_TRANSLATION); - } - - public IconAppChipView(Context context) { - this(context, null); - } - - public IconAppChipView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - Resources res = getResources(); - mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS); - mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true); - - // Menu dimensions - mCollapsedMenuDefaultWidth = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width); - mExpandedMenuDefaultWidth = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width); - mCollapsedMenuDefaultHeight = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height); - mExpandedMenuDefaultHeight = - res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height); - mIconMenuMarginTopStart = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin); - mMenuToChipGap = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_expanded_gap); - - // Background dimensions - mBackgroundMarginTopStart = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_background_margin_top_start); - - // Contents dimensions - mAppNameHorizontalMargin = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed); - mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin); - mIconViewMarginStart = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_view_start_margin); - mAppIconSize = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size); - mArrowSize = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_arrow_size); - mIconViewDrawableExpandedSize = res.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size); - - mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X, - INDEX_COUNT_TRANSLATION, - Float::sum); - mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y, - INDEX_COUNT_TRANSLATION, - Float::sum); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mIconView = findViewById(R.id.icon_view); - mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed); - mIconTextExpandedView = findViewById(R.id.icon_text_expanded); - mIconArrowView = findViewById(R.id.icon_arrow); - mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor); - } - - protected IconView getIconView() { - return mIconView; - } - - @Override - public void setText(CharSequence text) { - if (mIconTextCollapsedView != null) { - mIconTextCollapsedView.setText(text); - } - if (mIconTextExpandedView != null) { - mIconTextExpandedView.setText(text); - } - } - - @Override - public Drawable getDrawable() { - return mIconView == null ? null : mIconView.getDrawable(); - } - - @Override - public void setDrawable(Drawable icon) { - if (mIconView != null) { - mIconView.setDrawable(icon); - } - } - - @Override - public void setDrawableSize(int iconWidth, int iconHeight) { - if (mIconView != null) { - mIconView.setDrawableSize(iconWidth, iconHeight); - } - } - - /** - * Sets the maximum width of this Icon Menu. This is usually used when space is limited for - * split screen. - */ - public void setMaxWidth(int maxWidth) { - // Width showing only the app icon and arrow. Max width should not be set to less than this. - int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd; - mMaxWidth = Math.max(maxWidth, minimumMaxWidth); - } - - @Override - public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) { - RecentsPagedOrientationHandler orientationHandler = - orientationState.getOrientationHandler(); - // Layout params for anchor view - LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams(); - anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap; - mMenuAnchorView.setLayoutParams(anchorLayoutParams); - - // Layout Params for the Menu View (this) - LayoutParams iconMenuParams = (LayoutParams) getLayoutParams(); - iconMenuParams.width = mExpandedMenuDefaultWidth; - iconMenuParams.height = mExpandedMenuDefaultHeight; - orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart, - mIconMenuMarginTopStart); - setLayoutParams(iconMenuParams); - - // Layout params for the background - Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds(); - mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds); - setOutlineProvider(new ViewOutlineProvider() { - final Rect mRtlAppliedOutlineBounds = new Rect(); - @Override - public void getOutline(View view, Outline outline) { - mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation); - if (isLayoutRtl()) { - int width = getWidth(); - mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right; - mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left; - } - outline.setRoundRect( - mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f); - } - }); - - // Layout Params for the Icon View - LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams(); - int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart; - orientationHandler.setIconAppChipChildrenParams( - iconParams, iconMarginStartRelativeToParent); - - mIconView.setLayoutParams(iconParams); - mIconView.setDrawableSize(mAppIconSize, mAppIconSize); - - // Layout Params for the collapsed Icon Text View - int textMarginStart = - iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin; - LayoutParams iconTextCollapsedParams = - (LayoutParams) mIconTextCollapsedView.getLayoutParams(); - orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart); - int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart - - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd; - iconTextCollapsedParams.width = collapsedTextWidth; - mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams); - mIconTextCollapsedView.setAlpha(1f); - - // Layout Params for the expanded Icon Text View - LayoutParams iconTextExpandedParams = - (LayoutParams) mIconTextExpandedView.getLayoutParams(); - orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart); - mIconTextExpandedView.setLayoutParams(iconTextExpandedParams); - mIconTextExpandedView.setAlpha(0f); - mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth); - - // Layout Params for the Icon Arrow View - LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams(); - int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize; - orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart); - mIconArrowView.setPivotY(iconArrowParams.height / 2f); - mIconArrowView.setLayoutParams(iconArrowParams); - - // This method is called twice sometimes (like when rotating split tasks). It is called - // once before onMeasure and onLayout, and again after onMeasure but before onLayout with - // a new width. This happens because we update widths on rotation and on measure of - // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if - // it has just measured, so we explicitly call it here. - measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY)); - } - - @Override - public void setIconColorTint(int color, float amount) { - // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu. - float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR); - mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha); - } - - @Override - public void setContentAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha); - } - - @Override - public void setModalAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha); - } - - @Override - public int getDrawableWidth() { - return mIconView == null ? 0 : mIconView.getDrawableWidth(); - } - - @Override - public int getDrawableHeight() { - return mIconView == null ? 0 : mIconView.getDrawableHeight(); - } - - protected void revealAnim(boolean isRevealing) { - cancelInProgressAnimations(); - final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds(); - final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds(); - final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation); - mAnimator = new AnimatorSet(); - - if (isRevealing) { - boolean isRtl = isLayoutRtl(); - bringToFront(); - // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu - Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal( - mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, - mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth()); - // Animate background clipping - ValueAnimator backgroundAnimator = ValueAnimator.ofObject( - mBackgroundAnimationRectEvaluator, - initialBackground, - expandedBackgroundBounds); - backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline()); - - float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize; - float arrowTranslationX = - expandedBackgroundBounds.right - collapsedBackgroundBounds.right; - float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin; - float iconCenterToTextExpanded = - mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin; - float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed; - - float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX; - float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX; - - mAnimator.playTogether( - expandedTextRevealAnim, - backgroundAnimator, - ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling), - ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling), - ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, - textTranslationXWithRtl), - ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, - textTranslationXWithRtl), - ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0), - ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1), - ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl), - ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1)); - mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION); - } else { - // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu - Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal( - mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, - mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth()); - - // Animate background clipping - ValueAnimator backgroundAnimator = ValueAnimator.ofObject( - mBackgroundAnimationRectEvaluator, - initialBackground, - collapsedBackgroundBounds); - backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline()); - - mAnimator.playTogether( - expandedTextClipAnim, - backgroundAnimator, - ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1), - ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0), - ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0), - ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1), - ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0), - ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0), - ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1)); - mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION); - } - - mAnimator.setInterpolator(EMPHASIZED); - mAnimator.start(); - } - - private Rect getCollapsedBackgroundLtrBounds() { - Rect bounds = new Rect( - 0, - 0, - Math.min(mMaxWidth, mCollapsedMenuDefaultWidth), - mCollapsedMenuDefaultHeight); - bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart); - return bounds; - } - - private Rect getExpandedBackgroundLtrBounds() { - return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight); - } - - private void cancelInProgressAnimations() { - // We null the `AnimatorSet` because it holds references to the `Animators` which aren't - // expecting to be mutable and will cause a crash if they are re-used. - if (mAnimator != null && mAnimator.isStarted()) { - mAnimator.cancel(); - mAnimator = null; - } - } - - @Override - public View asView() { - return this; - } -} diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt new file mode 100644 index 0000000000..e14e6effe2 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt @@ -0,0 +1,681 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.RectEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Outline +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.TextUtils.TruncateAt +import android.util.AttributeSet +import android.view.Gravity +import android.view.KeyEvent +import android.view.View +import android.view.ViewAnimationUtils +import android.view.ViewOutlineProvider +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.animation.addListener +import androidx.core.view.updateLayoutParams +import com.android.app.animation.Interpolators +import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction +import com.android.launcher3.util.MultiValueAlpha +import com.android.quickstep.util.BorderAnimator +import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator +import com.android.quickstep.util.RecentsOrientedState +import kotlin.math.max +import kotlin.math.min + +/** An icon app menu view which can be used in place of an IconView in overview TaskViews. */ +class IconAppChipView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon { + + private var iconView: IconView? = null + private var iconArrowView: ImageView? = null + private var menuAnchorView: View? = null + + // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name. + private var appTitle: TextView? = null + private var isLayoutNaturalToLauncher = true + + private val backgroundRelativeLtrLocation = Rect() + private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation) + + // Menu dimensions + private val collapsedMenuDefaultWidth: Int = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width) + private val expandedMenuDefaultWidth: Int = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width) + private val collapsedMenuDefaultHeight = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height) + private val expandedMenuDefaultHeight = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height) + private val iconMenuMarginTopStart = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin) + private val menuToChipGap: Int = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap) + + // Background dimensions + val backgroundMarginTopStart: Int = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_background_margin_top_start + ) + + // Contents dimensions + private val appNameHorizontalMarginCollapsed = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed + ) + private val appNameHorizontalMarginExpanded = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_expanded + ) + private val arrowMarginEnd = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin) + private val iconViewMarginStart = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin) + private val appIconSize = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size) + private val iconMenuElevation = + resources.getDimension(R.dimen.task_thumbnail_icon_menu_elevation) + private val arrowSize = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size) + private val iconViewDrawableExpandedSize = + resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size) + private val focusBorderWidth = + resources.getDimensionPixelSize(R.dimen.app_chip_keyboard_border_width) + private val cornerRadius = resources.getDimensionPixelSize(R.dimen.app_chip_round_corner_radius) + + private var animator: AnimatorSet? = null + + private val multiValueAlpha: MultiValueAlpha = + MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) } + + private val viewTranslationX: MultiPropertyFactory = + MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR) + + private val viewTranslationY: MultiPropertyFactory = + MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR) + + // Width showing only the app icon and arrow. Max width should not be set to less than + // this. + private val minWidthAllowed = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd + var maxWidth = Integer.MAX_VALUE + /** + * Sets the maximum width of this Icon Menu. This is usually used when space is limited for + * split screen. + */ + set(value) { + // Width showing only the app icon and arrow. Max width should not be set to less than + // this. + field = max(value, minWidthAllowed) + } + + var status: AppChipStatus = AppChipStatus.Collapsed + private set + + val menuToCollapsedChipGap: Int = + getExpandedBackgroundLtrBounds().bottom - + getCollapsedBackgroundLtrBounds().bottom - + menuToChipGap + + private val focusBorderAnimator: BorderAnimator = + createSimpleBorderAnimator( + borderRadiusPx = cornerRadius, + borderWidthPx = focusBorderWidth, + boundsBuilder = { bounds -> + bounds.set(backgroundRelativeLtrLocation) + if (status == AppChipStatus.Expanded) { + // Draws the border inside the chip to avoid overlap with the task menu. + var inset = focusBorderWidth - 1 + bounds.inset(inset, inset) + } + }, + targetView = this, + borderColor = + context + .obtainStyledAttributes(attrs, R.styleable.IconAppChip) + .getColor( + R.styleable.IconAppChip_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + + private var focusAnimator: AnimatorSet? = null + + private fun animateFocusBorder(isAppearing: Boolean) { + focusAnimator?.cancel() + focusAnimator = null + val borderAnimator = focusBorderAnimator.buildAnimator(isAppearing) + + val initialBackground = Rect(backgroundRelativeLtrLocation) + val targetBackground: Rect = + when { + // Background animator to increase the clipping size to show the focus border. + isAppearing -> + Rect(backgroundRelativeLtrLocation).apply { + if (status == AppChipStatus.Collapsed) + inset(-focusBorderWidth + 1, -focusBorderWidth + 1) + } + // Background animator to restore the outline size to hide the focus border + status == AppChipStatus.Expanded -> getExpandedBackgroundLtrBounds() + else -> getCollapsedBackgroundLtrBounds() + } + val backgroundAnimator = + ValueAnimator.ofObject( + backgroundAnimationRectEvaluator, + initialBackground, + targetBackground, + ) + .apply { addUpdateListener { invalidateOutline() } } + + focusAnimator = + AnimatorSet().apply { + playTogether(borderAnimator, backgroundAnimator) + duration = borderAnimator.duration + interpolator = borderAnimator.interpolator + start() + } + } + + public override fun onFocusChanged( + gainFocus: Boolean, + direction: Int, + previouslyFocusedRect: Rect?, + ) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + animateFocusBorder(isAppearing = gainFocus) + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + focusBorderAnimator.drawBorder(canvas) + } + + override fun onFinishInflate() { + super.onFinishInflate() + iconView = findViewById(R.id.icon_view) + appTitle = findViewById(R.id.icon_title) + iconArrowView = findViewById(R.id.icon_arrow) + menuAnchorView = findViewById(R.id.icon_view_menu_anchor) + } + + override fun setText(text: CharSequence?) { + if (text == appTitle?.text) return + appTitle?.text = text + } + + override fun getDrawable(): Drawable? = iconView?.drawable + + private var currentIconDrawableHash: Int = 0 + + override fun setDrawable(icon: Drawable?) { + if (icon.hashCode() == currentIconDrawableHash) return + iconView?.drawable = icon + currentIconDrawableHash = icon.hashCode() + } + + override fun setDrawableSize(iconWidth: Int, iconHeight: Int) { + iconView?.setDrawableSize(iconWidth, iconHeight) + } + + override fun getMinimumWidth(): Int = min(maxWidth, collapsedMenuDefaultWidth) + + override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) { + val orientationHandler = orientationState.orientationHandler + isLayoutNaturalToLauncher = orientationHandler.isLayoutNaturalToLauncher + // Layout params for anchor view + val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams + if (orientationHandler.isLayoutNaturalToLauncher) { + anchorLayoutParams.gravity = Gravity.START + anchorLayoutParams.marginStart = backgroundMarginTopStart + } else { + anchorLayoutParams.gravity = Gravity.LEFT + anchorLayoutParams.marginStart = 0 + } + anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap + menuAnchorView!!.layoutParams = anchorLayoutParams + + // Layout Params for the Menu View (this) + val iconMenuParams = layoutParams as LayoutParams + iconMenuParams.width = getChipWidth() + iconMenuParams.height = expandedMenuDefaultHeight + orientationHandler.setIconAppChipMenuParams( + this, + iconMenuParams, + iconMenuMarginTopStart, + iconMenuMarginTopStart, + ) + layoutParams = iconMenuParams + + // Layout params for the background + val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds() + backgroundRelativeLtrLocation.set(collapsedBackgroundBounds) + outlineProvider = + object : ViewOutlineProvider() { + val mRtlAppliedOutlineBounds: Rect = Rect() + + override fun getOutline(view: View, outline: Outline) { + mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation) + if (isLayoutRtl) { + val width = width + mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right + mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left + } + outline.setRoundRect( + mRtlAppliedOutlineBounds, + resources.getDimension(R.dimen.app_chip_round_corner_radius), + ) + } + } + + // Layout Params for the Icon View + val iconParams = iconView!!.layoutParams as LayoutParams + val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart + orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent) + + iconView!!.layoutParams = iconParams + iconView!!.setDrawableSize(appIconSize, appIconSize) + + // Layout Params for the collapsed Icon Text View + val textMarginStart = + iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMarginCollapsed + val iconTextCollapsedParams = appTitle!!.layoutParams as LayoutParams + orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart) + iconTextCollapsedParams.width = + calculateCollapsedTextWidth(collapsedBackgroundBounds.width()) + appTitle?.layoutParams = iconTextCollapsedParams + + // Layout Params for the Icon Arrow View + val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams + val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize + orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart) + iconArrowView!!.pivotY = iconArrowParams.height / 2f + iconArrowView!!.layoutParams = iconArrowParams + + // This method is called twice sometimes (like when rotating split tasks). It is called + // once before onMeasure and onLayout, and again after onMeasure but before onLayout with + // a new width. This happens because we update widths on rotation and on measure of + // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if + // it has just measured, so we explicitly call it here. + measure( + MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY), + ) + } + + private fun enableMarquee(isEnabled: Boolean) { + // Marquee should not be enabled when is running test harness. + val isMarqueeEnabled = isEnabled && !Utilities.isRunningInTestHarness() + appTitle?.let { + it.ellipsize = if (isMarqueeEnabled) TruncateAt.MARQUEE else null + it.isSelected = isMarqueeEnabled + } + } + + /** + * Calculates the width available for the collapsed text (app name) within the view. + * + * This function determines the maximum width that the app name can occupy when the view is in + * its collapsed state. It considers various factors such as the maximum allowed width, the + * bounds of the collapsed background, the size of the app icon, the arrow, and the margins + * around these elements. + * + * @return The calculated width available for the collapsed text (app name). + */ + private fun calculateCollapsedTextWidth(width: Int): Int { + val collapsedTextWidth = + (width - + iconViewMarginStart - + appIconSize - + arrowSize - + appNameHorizontalMarginCollapsed - + arrowMarginEnd) + + val spaceLeftForText = maxWidth - minWidthAllowed + return minOf(collapsedTextWidth, spaceLeftForText).coerceAtLeast(0) + } + + private fun calculateExpandedTextWidth(width: Int): Int = + width - + iconViewMarginStart - + iconViewDrawableExpandedSize - + arrowSize - + appNameHorizontalMarginExpanded - + arrowMarginEnd + + override fun setIconColorTint(color: Int, amount: Float) { + // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu. + val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR) + multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha + } + + override fun setContentAlpha(alpha: Float) { + multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha + } + + override fun setModalAlpha(alpha: Float) { + multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha + } + + override fun setFlexSplitAlpha(alpha: Float) { + multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha + } + + override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0 + + override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0 + + /** Gets the view split x-axis translation */ + fun getSplitTranslationX(): MultiPropertyFactory.MultiProperty = + viewTranslationX.get(INDEX_SPLIT_TRANSLATION) + + /** + * Sets the view split x-axis translation + * + * @param value x-axis translation + */ + fun setSplitTranslationX(value: Float) { + getSplitTranslationX().value = value + } + + /** Gets the view split y-axis translation */ + fun getSplitTranslationY(): MultiPropertyFactory.MultiProperty = + viewTranslationY[INDEX_SPLIT_TRANSLATION] + + /** + * Sets the view split y-axis translation + * + * @param value y-axis translation + */ + fun setSplitTranslationY(value: Float) { + getSplitTranslationY().value = value + } + + /** Gets the menu x-axis translation for split task */ + fun getMenuTranslationX(): MultiPropertyFactory.MultiProperty = + viewTranslationX[INDEX_MENU_TRANSLATION] + + /** Gets the menu y-axis translation for split task */ + fun getMenuTranslationY(): MultiPropertyFactory.MultiProperty = + viewTranslationY[INDEX_MENU_TRANSLATION] + + internal fun revealAnim(isRevealing: Boolean, animated: Boolean = true): AnimatorSet { + cancelInProgressAnimations() + val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds() + val expandedBackgroundBounds = getExpandedBackgroundLtrBounds() + val initialBackground = Rect(backgroundRelativeLtrLocation) + animator = AnimatorSet() + + val isRtl = isLayoutRtl + if (isRevealing) { + bringToFront() + // Animate background clipping + val backgroundAnimator = + ValueAnimator.ofObject( + backgroundAnimationRectEvaluator, + initialBackground, + expandedBackgroundBounds, + ) + backgroundAnimator.addUpdateListener { invalidateOutline() } + + val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat() + val arrowTranslationX = + (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat() + val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMarginCollapsed + val iconCenterToTextExpanded = + iconViewDrawableExpandedSize / 2f + appNameHorizontalMarginCollapsed + val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed + + val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX + val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX + + animator!!.playTogether( + backgroundAnimator, + ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling), + ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling), + ObjectAnimator.ofFloat(appTitle, TRANSLATION_X, textTranslationXWithRtl), + ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl), + ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f), + ) + animator!!.duration = MENU_BACKGROUND_REVEAL_DURATION.toLong() + status = AppChipStatus.Expanded + } else { + // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu + val expandedTextClipAnim = + ViewAnimationUtils.createCircularReveal( + appTitle, + if (isRtl) appTitle!!.width else 0, + appTitle!!.height / 2, + appTitle!!.width.toFloat(), + calculateCollapsedTextWidth(collapsedBackgroundBounds.width()).toFloat(), + ) + + // Animate background clipping + val backgroundAnimator = + ValueAnimator.ofObject( + backgroundAnimationRectEvaluator, + initialBackground, + collapsedBackgroundBounds, + ) + backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? -> + invalidateOutline() + } + + animator!!.playTogether( + expandedTextClipAnim, + backgroundAnimator, + ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f), + ObjectAnimator.ofFloat(appTitle, TRANSLATION_X, 0f), + ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f), + ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f), + ) + animator!!.duration = MENU_BACKGROUND_HIDE_DURATION.toLong() + status = AppChipStatus.Collapsed + sendToBack() + } + + if (!animated) animator!!.duration = 0 + animator!!.interpolator = Interpolators.EMPHASIZED + + // Increase the chip and appTitle size before the animation starts when it's expanding. + // And decrease the size after the animation when is collapsing. + animator!!.addListener( + onStart = { + // Hide focused border during expanding/collapsing animation + if (isFocused) { + focusBorderAnimator.setBorderVisibility(visible = false, animated = false) + } + when (status) { + AppChipStatus.Expanded -> updateChipSize() + // Disable marquee before chip is collapsed + AppChipStatus.Collapsed -> enableMarquee(false) + } + }, + onEnd = { + if (isFocused) animateFocusBorder(isAppearing = true) + when (status) { + AppChipStatus.Collapsed -> updateChipSize() + // Enable marquee after chip is fully expanded + AppChipStatus.Expanded -> enableMarquee(true) + } + }, + ) + return animator!! + } + + /** + * Updates the width of the app title based on the current [AppChipStatus]. + * + * This function dynamically adjusts the width of the `appTitle` TextView depending on whether + * the app chip is in an expanded or collapsed state. + * - When the chip is [AppChipStatus.Expanded], the title width is set to + * [expandedMaxTextWidth], allowing the title to potentially take up more space. + * - When the chip is [AppChipStatus.Collapsed], the title width is calculated based on the + * width of the collapsed background. This ensures the title fits within the smaller, + * collapsed chip boundaries. The width is then determined by calling + * [calculateCollapsedTextWidth]. + */ + private fun updateChipSize() { + val chipWidth = getChipWidth() + when (status) { + AppChipStatus.Expanded -> { + updateLayoutParams { width = chipWidth } + appTitle!!.updateLayoutParams { width = calculateExpandedTextWidth(chipWidth) } + } + AppChipStatus.Collapsed -> { + appTitle!!.updateLayoutParams { + val collapsedBackgroundWidth = getCollapsedBackgroundLtrBounds().width() + width = calculateCollapsedTextWidth(collapsedBackgroundWidth) + } + updateLayoutParams { width = chipWidth } + } + } + } + + private fun getCollapsedBackgroundLtrBounds(): Rect { + val bounds = Rect(0, 0, minimumWidth, collapsedMenuDefaultHeight) + bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart) + return bounds + } + + private fun getExpandedBackgroundLtrBounds() = + Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight) + + private fun getCollapsedBackgroundWidth() = getCollapsedBackgroundLtrBounds().right + + private fun getChipWidth(): Int { + // TODO(b/292269949): When in fake orientation, the width of the chip remains expanded + // to prevent wrong translation due to chip rotation and anchor. + if (!isLayoutNaturalToLauncher) return expandedMenuDefaultWidth + return when (status) { + AppChipStatus.Expanded -> expandedMenuDefaultWidth + AppChipStatus.Collapsed -> getCollapsedBackgroundWidth() + } + } + + private fun cancelInProgressAnimations() { + // We null the `AnimatorSet` because it holds references to the `Animators` which aren't + // expecting to be mutable and will cause a crash if they are re-used. + if (animator != null && animator!!.isStarted) { + animator!!.cancel() + animator = null + } + } + + override fun bringToFront() { + super.bringToFront() + z = iconMenuElevation + Z_INDEX_FRONT + updateParentZIndex(Z_INDEX_FRONT) + } + + private fun sendToBack() { + z = iconMenuElevation + updateParentZIndex(0f) + } + + private fun updateParentZIndex(zIndex: Float) { + val parentView = parent as? TaskView + if (parentView?.isOnGridBottomRow == true) { + parentView.z = zIndex + } + } + + override fun focusSearch(direction: Int): View? { + if (mParent == null) return null + return when (direction) { + FOCUS_RIGHT, + FOCUS_DOWN -> mParent.focusSearch(this, FOCUS_FORWARD) + FOCUS_UP, + FOCUS_LEFT -> mParent.focusSearch(this, FOCUS_BACKWARD) + else -> super.focusSearch(direction) + } + } + + /** + * We need to over-ride here due to liveTile mode, the [OverviewInputConsumer] is added, which + * consumes all [InputEvent]'s and focus isn't moved correctly. + */ + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action != KeyEvent.ACTION_DOWN) return super.dispatchKeyEvent(event) + + val currentFocus = findFocus() ?: return super.dispatchKeyEvent(event) + + val nextFocus = + when (event.keyCode) { + KeyEvent.KEYCODE_DPAD_UP -> focusSearch(currentFocus, FOCUS_BACKWARD) + KeyEvent.KEYCODE_DPAD_DOWN -> focusSearch(currentFocus, FOCUS_FORWARD) + KeyEvent.KEYCODE_TAB -> + focusSearch( + currentFocus, + if (event.isShiftPressed) FOCUS_BACKWARD else FOCUS_FORWARD, + ) + else -> null + } + + return nextFocus?.requestFocus() ?: super.dispatchKeyEvent(event) + } + + fun reset() { + setText(null) + drawable = null + } + + override fun asView(): View = this + + enum class AppChipStatus { + Expanded, + Collapsed, + } + + private companion object { + private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b } + + private const val MENU_BACKGROUND_REVEAL_DURATION = 417 + private const val MENU_BACKGROUND_HIDE_DURATION = 333 + + private const val Z_INDEX_FRONT = 10f + + private const val NUM_ALPHA_CHANNELS = 4 + private const val INDEX_CONTENT_ALPHA = 0 + private const val INDEX_COLOR_FILTER_ALPHA = 1 + private const val INDEX_MODAL_ALPHA = 2 + /** Used to hide the app chip for 90:10 flex split. */ + private const val INDEX_MINIMUM_RATIO_ALPHA = 3 + + private const val INDEX_SPLIT_TRANSLATION = 0 + private const val INDEX_MENU_TRANSLATION = 1 + private const val INDEX_COUNT_TRANSLATION = 2 + } +} diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java deleted file mode 100644 index bb4a7ecda6..0000000000 --- a/quickstep/src/com/android/quickstep/views/IconView.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.widget.FrameLayout; - -import androidx.annotation.Nullable; - -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Utilities; -import com.android.launcher3.util.MultiValueAlpha; -import com.android.launcher3.views.ActivityContext; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.util.RecentsOrientedState; - -/** - * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout - * when the drawable changes. - */ -public class IconView extends View implements TaskViewIcon { - private static final int NUM_ALPHA_CHANNELS = 2; - private static final int INDEX_CONTENT_ALPHA = 0; - private static final int INDEX_MODAL_ALPHA = 1; - - private final MultiValueAlpha mMultiValueAlpha; - - @Nullable - private Drawable mDrawable; - private int mDrawableWidth, mDrawableHeight; - - public IconView(Context context) { - this(context, null); - } - - public IconView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public IconView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS); - mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true); - } - - /** - * Sets a {@link Drawable} to be displayed. - */ - @Override - public void setDrawable(@Nullable Drawable d) { - if (mDrawable != null) { - mDrawable.setCallback(null); - } - mDrawable = d; - if (mDrawable != null) { - mDrawable.setCallback(this); - setDrawableSizeInternal(getWidth(), getHeight()); - } - invalidate(); - } - - /** - * Sets the size of the icon drawable. - */ - @Override - public void setDrawableSize(int iconWidth, int iconHeight) { - mDrawableWidth = iconWidth; - mDrawableHeight = iconHeight; - if (mDrawable != null) { - setDrawableSizeInternal(getWidth(), getHeight()); - } - } - - private void setDrawableSizeInternal(int selfWidth, int selfHeight) { - Rect selfRect = new Rect(0, 0, selfWidth, selfHeight); - Rect drawableRect = new Rect(); - Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect); - mDrawable.setBounds(drawableRect); - } - - @Override - @Nullable - public Drawable getDrawable() { - return mDrawable; - } - - @Override - public int getDrawableWidth() { - return mDrawableWidth; - } - - @Override - public int getDrawableHeight() { - return mDrawableHeight; - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - if (mDrawable != null) { - setDrawableSizeInternal(w, h); - } - } - - @Override - protected boolean verifyDrawable(Drawable who) { - return super.verifyDrawable(who) || who == mDrawable; - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - - final Drawable drawable = mDrawable; - if (drawable != null && drawable.isStateful() - && drawable.setState(getDrawableState())) { - invalidateDrawable(drawable); - } - } - - @Override - protected void onDraw(Canvas canvas) { - if (mDrawable != null) { - mDrawable.draw(canvas); - } - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } - - @Override - public void setContentAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha); - } - - @Override - public void setModalAlpha(float alpha) { - mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha); - } - - /** - * Set the tint color of the icon, useful for scrimming or dimming. - * - * @param color to blend in. - * @param amount [0,1] 0 no tint, 1 full tint - */ - @Override - public void setIconColorTint(int color, float amount) { - if (mDrawable != null) { - mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount)); - } - } - - @Override - public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) { - RecentsPagedOrientationHandler orientationHandler = - orientationState.getOrientationHandler(); - boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; - DeviceProfile deviceProfile = - ActivityContext.lookupContext(getContext()).getDeviceProfile(); - - FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams(); - - int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; - int taskIconHeight = deviceProfile.overviewTaskIconSizePx; - int taskMargin = deviceProfile.overviewTaskMarginPx; - - orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight, - thumbnailTopMargin, isRtl); - iconParams.width = iconParams.height = taskIconHeight; - setLayoutParams(iconParams); - - setRotation(orientationHandler.getDegreesRotated()); - int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx - : deviceProfile.overviewTaskIconDrawableSizePx; - setDrawableSize(iconDrawableSize, iconDrawableSize); - } - - @Override - public View asView() { - return this; - } -} diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt new file mode 100644 index 0000000000..04f9537617 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/IconView.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.updateLayoutParams +import com.android.launcher3.DeviceProfile +import com.android.launcher3.Flags +import com.android.launcher3.Utilities +import com.android.launcher3.util.MSDLPlayerWrapper +import com.android.launcher3.util.MultiValueAlpha +import com.android.launcher3.views.ActivityContext +import com.android.quickstep.util.RecentsOrientedState +import com.google.android.msdl.data.model.MSDLToken + +/** + * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout + * when the drawable changes. + */ +class IconView : View, TaskViewIcon { + private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS) + private var drawable: Drawable? = null + private var drawableWidth = 0 + private var drawableHeight = 0 + private var msdlPlayerWrapper: MSDLPlayerWrapper? = null + + constructor(context: Context) : super(context) { + setUpHaptics() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + setUpHaptics() + } + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) { + setUpHaptics() + } + + init { + multiValueAlpha.setUpdateVisibility(true) + } + + private fun setUpHaptics() { + msdlPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context) + // Haptics are handled by the MSDLPlayerWrapper + isHapticFeedbackEnabled = !Flags.msdlFeedback() || msdlPlayerWrapper == null + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + super.setOnLongClickListener(l?.withFeedback()) + } + + /** Sets a [Drawable] to be displayed. */ + override fun setDrawable(d: Drawable?) { + drawable?.callback = null + + // Copy drawable so that mutations below do not affect other users of the drawable + drawable = d?.constantState?.newDrawable()?.mutate() + drawable?.let { + it.callback = this + setDrawableSizeInternal(width, height) + } + invalidate() + } + + /** Sets the size of the icon drawable. */ + override fun setDrawableSize(iconWidth: Int, iconHeight: Int) { + drawableWidth = iconWidth + drawableHeight = iconHeight + drawable?.let { setDrawableSizeInternal(width, height) } + } + + private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) { + val selfRect = Rect(0, 0, selfWidth, selfHeight) + val drawableRect = Rect() + Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect) + drawable?.bounds = drawableRect + } + + override fun getDrawable(): Drawable? = drawable + + override fun getDrawableWidth(): Int = drawableWidth + + override fun getDrawableHeight(): Int = drawableHeight + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + drawable?.let { setDrawableSizeInternal(w, h) } + } + + override fun verifyDrawable(who: Drawable): Boolean = + super.verifyDrawable(who) || who === drawable + + override fun drawableStateChanged() { + super.drawableStateChanged() + drawable?.let { + if (it.isStateful && it.setState(drawableState)) { + invalidateDrawable(it) + } + } + } + + override fun onDraw(canvas: Canvas) { + drawable?.draw(canvas) + } + + override fun hasOverlappingRendering(): Boolean = false + + override fun setContentAlpha(alpha: Float) { + multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha) + } + + override fun setModalAlpha(alpha: Float) { + multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha) + } + + override fun setFlexSplitAlpha(alpha: Float) { + multiValueAlpha[INDEX_FLEX_SPLIT_ALPHA].setValue(alpha) + } + + /** + * Set the tint color of the icon, useful for scrimming or dimming. + * + * @param color to blend in. + * @param amount [0,1] 0 no tint, 1 full tint + */ + override fun setIconColorTint(color: Int, amount: Float) { + drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount) + } + + override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) { + val orientationHandler = orientationState.orientationHandler + val deviceProfile: DeviceProfile = + (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile() + orientationHandler.setTaskIconParams( + iconParams = layoutParams as FrameLayout.LayoutParams, + taskIconMargin = deviceProfile.overviewProfile.taskMarginPx, + taskIconHeight = deviceProfile.overviewProfile.taskIconSizePx, + thumbnailTopMargin = deviceProfile.overviewProfile.taskThumbnailTopMarginPx, + isRtl = layoutDirection == LAYOUT_DIRECTION_RTL, + ) + updateLayoutParams { + height = deviceProfile.overviewProfile.taskIconSizePx + width = height + } + setRotation(orientationHandler.degreesRotated) + val iconDrawableSize = + if (isGridTask) deviceProfile.overviewProfile.taskIconDrawableSizeGridPx + else deviceProfile.overviewProfile.taskIconDrawableSizePx + setDrawableSize(iconDrawableSize, iconDrawableSize) + } + + override fun asView(): View = this + + private fun OnLongClickListener.withFeedback(): OnLongClickListener { + val delegate = this + return OnLongClickListener { v: View -> + if (Flags.msdlFeedback()) { + msdlPlayerWrapper?.playToken(MSDLToken.LONG_PRESS) + } + delegate.onLongClick(v) + } + } + + companion object { + private const val NUM_ALPHA_CHANNELS = 3 + private const val INDEX_CONTENT_ALPHA = 0 + private const val INDEX_MODAL_ALPHA = 1 + private const val INDEX_FLEX_SPLIT_ALPHA = 2 + } +} diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index b4bca1ced8..e8753b9702 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -16,17 +16,16 @@ package com.android.quickstep.views; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; -import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.ADD_DESK_BUTTON; import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON; -import static com.android.launcher3.LauncherState.EDIT_MODE; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK; import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT; -import static com.android.launcher3.LauncherState.SPRING_LOADED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME; -import static com.android.window.flags2.Flags.enableDesktopWindowingWallpaperActivity; +import static com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview; import android.annotation.TargetApi; import android.content.Context; @@ -40,7 +39,6 @@ import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.desktop.DesktopRecentsTransitionController; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.statehandlers.DepthController; @@ -52,11 +50,10 @@ import com.android.launcher3.util.PendingSplitSelectInfo; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; import com.android.quickstep.GestureState; -import com.android.quickstep.LauncherActivityInterface; -import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.util.AnimUtils; import com.android.quickstep.util.SplitSelectStateController; -import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.GroupedTaskInfo; import kotlin.Unit; @@ -76,7 +73,7 @@ public class LauncherRecentsView extends RecentsView remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true)); + mBlurUtils.setDrawLiveTileBelowRecents(true); } } @@ -200,7 +211,10 @@ public class LauncherRecentsView extends RecentsView extends FrameLayo public static final String TAG = "OverviewActionsView"; private final Rect mInsets = new Rect(); + /** + * We need to over-ride here due to liveTile mode, the [OverviewInputConsumer] is added, which + * consumes all [InputEvent]'s and focus isn't moved correctly. + */ + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) return super.dispatchKeyEvent(event); + + View currentFocus = findFocus(); + if (currentFocus == null) return super.dispatchKeyEvent(event); + + View nextFocus = null; + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT -> nextFocus = focusSearch(currentFocus, + FOCUS_BACKWARD); + case KeyEvent.KEYCODE_DPAD_RIGHT -> nextFocus = focusSearch(currentFocus, + FOCUS_FORWARD); + case KeyEvent.KEYCODE_TAB -> nextFocus = focusSearch(currentFocus, + event.isShiftPressed() ? FOCUS_BACKWARD : FOCUS_FORWARD); + } + + if (nextFocus != null) { + return nextFocus.requestFocus(); + } + + return super.dispatchKeyEvent(event); + } + @IntDef(flag = true, value = { HIDDEN_NON_ZERO_ROTATION, HIDDEN_NO_TASKS, @@ -65,8 +97,7 @@ public class OverviewActionsView extends FrameLayo HIDDEN_DESKTOP }) @Retention(RetentionPolicy.SOURCE) - public @interface ActionsHiddenFlags { - } + public @interface ActionsHiddenFlags { } public static final int HIDDEN_NON_ZERO_ROTATION = 1 << 0; public static final int HIDDEN_NO_TASKS = 1 << 1; @@ -79,10 +110,9 @@ public class OverviewActionsView extends FrameLayo @IntDef(flag = true, value = { DISABLED_SCROLLING, DISABLED_ROTATED, - DISABLED_NO_THUMBNAIL }) + DISABLED_NO_THUMBNAIL}) @Retention(RetentionPolicy.SOURCE) - public @interface ActionsDisabledFlags { - } + public @interface ActionsDisabledFlags { } public static final int DISABLED_SCROLLING = 1 << 0; public static final int DISABLED_ROTATED = 1 << 1; @@ -98,14 +128,11 @@ public class OverviewActionsView extends FrameLayo private static final int INDEX_3P_LAUNCHER = 7; private static final int NUM_ALPHAS = 8; - public @interface SplitButtonHiddenFlags { - } - + public @interface SplitButtonHiddenFlags { } public static final int FLAG_SMALL_SCREEN_HIDE_SPLIT = 1 << 0; /** - * Holds an AnimatedFloat for each alpha property, used to set or animate alpha - * values in + * Holds an AnimatedFloat for each alpha property, used to set or animate alpha values in * {@link #mMultiValueAlphas}. */ private final AnimatedFloat[] mAlphaProperties = new AnimatedFloat[NUM_ALPHAS]; @@ -117,14 +144,11 @@ public class OverviewActionsView extends FrameLayo /** Index used for grouped-task actions in the mMultiValueAlphas array */ private static final int GROUP_ACTIONS_ALPHAS = 1; - /** - * Container for the action buttons below a focused, non-split Overview tile. - */ + /** Container for the action buttons below a focused, non-split Overview tile. */ protected LinearLayout mActionButtons; private Button mSplitButton; /** - * The "save app pair" button. Currently this is the only button that is not - * contained in + * The "save app pair" button. Currently this is the only button that is not contained in * mActionButtons, since it is the sole button that appears for a grouped task. */ private Button mSaveAppPairButton; @@ -163,18 +187,17 @@ public class OverviewActionsView extends FrameLayo protected void onFinishInflate() { super.onFinishInflate(); // Initialize 2 view containers: one for single tasks, one for grouped tasks. - // These will take up the same space on the screen and alternate visibility as - // needed. + // These will take up the same space on the screen and alternate visibility as needed. // Currently, the only grouped task action is "save app pairs". mActionButtons = findViewById(R.id.action_buttons); mSaveAppPairButton = findViewById(R.id.action_save_app_pair); - // Initialize a list to hold alphas for mActionButtons and any group action - // buttons. + TypefaceUtils.setTypeface(mSaveAppPairButton, FontFamily.GSF_LABEL_LARGE); + // Initialize a list to hold alphas for mActionButtons and any group action buttons. mMultiValueAlphas[ACTIONS_ALPHAS] = new MultiValueAlpha(mActionButtons, NUM_ALPHAS); - mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] = new MultiValueAlpha(mSaveAppPairButton, NUM_ALPHAS); + mMultiValueAlphas[GROUP_ACTIONS_ALPHAS] = + new MultiValueAlpha(mSaveAppPairButton, NUM_ALPHAS); Arrays.stream(mMultiValueAlphas).forEach(a -> a.setUpdateVisibility(true)); - // To control alpha simultaneously on mActionButtons and any group action - // buttons, we set up + // To control alpha simultaneously on mActionButtons and any group action buttons, we set up // an AnimatedFloat for each alpha property. for (int i = 0; i < NUM_ALPHAS; i++) { final int index = i; @@ -185,10 +208,8 @@ public class OverviewActionsView extends FrameLayo }, 1f /* initialValue */); } - // The screenshot button is implemented as a Button in launcher3 and - // NexusLauncher, but is - // an ImageButton in go launcher (does not share a common class with Button). - // Take care when + // The screenshot button is implemented as a Button in launcher3 and NexusLauncher, but is + // an ImageButton in go launcher (does not share a common class with Button). Take care when // casting this. View screenshotButton = findViewById(R.id.action_screenshot); screenshotButton.setOnClickListener(this); @@ -245,15 +266,12 @@ public class OverviewActionsView extends FrameLayo } /** - * Updates the proper disabled flag to indicate whether OverviewActionsView - * should be enabled. - * Ignores DISABLED_ROTATED flag for determining enabled. Flag is used to - * enable/disable + * Updates the proper disabled flag to indicate whether OverviewActionsView should be enabled. + * Ignores DISABLED_ROTATED flag for determining enabled. Flag is used to enable/disable * buttons individually, currently done for select button in subclass. * * @param disabledFlags The flag to update. - * @param enable Whether to enable the disable flag: True will cause view - * to be disabled. + * @param enable Whether to enable the disable flag: True will cause view to be disabled. */ public void updateDisabledFlags(@ActionsDisabledFlags int disabledFlags, boolean enable) { if (enable) { @@ -266,14 +284,11 @@ public class OverviewActionsView extends FrameLayo } /** - * Updates a batch of flags to hide and show actions buttons when a grouped task - * (split screen) + * Updates a batch of flags to hide and show actions buttons when a grouped task (split screen) * is focused. - * - * @param isGroupedTask True if the focused task is a grouped task. - * @param canSaveAppPair True if the focused task is a grouped task and can be - * saved as an app - * pair. + * @param isGroupedTask True if the focused task is a grouped task. + * @param canSaveAppPair True if the focused task is a grouped task and can be saved as an app + * pair. */ public void updateForGroupedTask(boolean isGroupedTask, boolean canSaveAppPair) { Log.d(TAG, "updateForGroupedTask() called with: isGroupedTask = [" + isGroupedTask @@ -284,20 +299,21 @@ public class OverviewActionsView extends FrameLayo } /** - * Updates a batch of flags to hide and show actions buttons for tablet/non - * tablet case. + * Updates a batch of flags to hide and show actions buttons for tablet/non tablet case. */ private void updateForIsTablet() { assert mDp != null; // Update flags to see if split button should be hidden. - updateSplitButtonHiddenFlags(FLAG_SMALL_SCREEN_HIDE_SPLIT, !mDp.isTablet); + updateSplitButtonHiddenFlags(FLAG_SMALL_SCREEN_HIDE_SPLIT, !mDp.getDeviceProperties().isTablet()); updateActionButtonsVisibility(); } private void updateActionButtonsVisibility() { - assert mDp != null; + if (mDp == null) { + return; + } boolean showSingleTaskActions = !mIsGroupedTask; - boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair; + boolean showGroupActions = mIsGroupedTask && mDp.getDeviceProperties().isTablet() && mCanSaveAppPair; Log.d(TAG, "updateActionButtonsVisibility() called: showSingleTaskActions = [" + showSingleTaskActions + "], showGroupActions = [" + showGroupActions + "]"); getActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showSingleTaskActions ? 1 : 0); @@ -320,17 +336,14 @@ public class OverviewActionsView extends FrameLayo } /** - * Updates the proper flags to indicate whether the "Split screen" button should - * be hidden. + * Updates the proper flags to indicate whether the "Split screen" button should be hidden. * * @param flag The flag to update. - * @param enable Whether to enable the hidden flag: True will cause view to be - * hidden. + * @param enable Whether to enable the hidden flag: True will cause view to be hidden. */ void updateSplitButtonHiddenFlags(@SplitButtonHiddenFlags int flag, boolean enable) { - if (mSplitButton == null) - return; + if (mSplitButton == null) return; if (enable) { mSplitButtonHiddenFlags |= flag; } else { @@ -372,19 +385,14 @@ public class OverviewActionsView extends FrameLayo } /** - * Offsets OverviewActionsView horizontal position based on 3 button nav - * container in taskbar. + * Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar. */ private void updatePadding() { - // If taskbar is in overview, overview action has dedicated space above nav - // buttons + // If taskbar is in overview, overview action has dedicated space above nav buttons setPadding(mInsets.left, 0, mInsets.right, 0); } - /** - * Updates vertical margins for different navigation mode or configuration - * changes. - */ + /** Updates vertical margins for different navigation mode or configuration changes. */ public void updateVerticalMargin(NavigationMode mode) { updateActionBarPosition(mActionButtons); updateActionBarPosition(mSaveAppPairButton); @@ -398,7 +406,7 @@ public class OverviewActionsView extends FrameLayo LayoutParams actionParams = (LayoutParams) actionBar.getLayoutParams(); actionParams.setMargins( - actionParams.leftMargin, mDp.overviewActionsTopMarginPx, + actionParams.leftMargin, mDp.getOverviewProfile().getActionsTopMarginPx(), actionParams.rightMargin, getBottomMargin()); } @@ -407,13 +415,15 @@ public class OverviewActionsView extends FrameLayo return 0; } - if (mDp.isTablet && Flags.enableGridOnlyOverview()) { - return mDp.stashedTaskbarHeight; + if (mDp.getDeviceProperties().isTablet() && enableGridOnlyOverview()) { + return mDp.getTaskbarProfile().getStashedTaskbarHeight(); } // Align to bottom of task Rect. - return mDp.heightPx - mTaskSize.bottom - mDp.overviewActionsTopMarginPx - - mDp.overviewActionsHeight; + return mDp.getDeviceProperties().getHeightPx() + - mTaskSize.bottom + - mDp.getOverviewProfile().getActionsTopMarginPx() + - mDp.getOverviewProfile().getActionsHeight(); } /** diff --git a/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt new file mode 100644 index 0000000000..972f187680 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt @@ -0,0 +1,1387 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.view.View +import androidx.core.graphics.toRectF +import androidx.core.view.children +import androidx.core.view.contains +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.FloatValueHolder +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.internal.jank.Cuj +import com.android.launcher3.PagedView +import com.android.launcher3.R +import com.android.launcher3.concurrent.annotations.LightweightBackground +import com.android.launcher3.concurrent.annotations.LightweightBackgroundPriority +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.util.DynamicResource +import com.android.launcher3.util.MSDLPlayerWrapper +import com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview +import com.android.launcher3.views.ActivityContext +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled +import com.android.quickstep.util.TaskGridNavHelper +import com.android.quickstep.util.isDefaultDisplay +import com.android.quickstep.util.isExternalDisplay +import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY +import com.android.quickstep.views.RecentsViewUtils.OnDeskAddedListener +import com.android.quickstep.views.TaskView.Companion.GRID_END_TRANSLATION_X +import com.android.systemui.shared.system.ActivityManagerWrapper +import com.android.systemui.shared.system.InteractionJankMonitorWrapper +import com.google.android.msdl.data.model.MSDLToken +import com.google.common.util.concurrent.ListeningExecutorService +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt +import kotlin.math.sign + +/** + * Helper class for [RecentsView]. This util class contains refactored and extracted functions from + * RecentsView related to TaskView dismissal. + */ +class RecentsDismissUtils +@AssistedInject +constructor( + @Assisted private val recentsView: RecentsView<*, *>, + private val systemUiProxy: SystemUiProxy, + @LightweightBackground(LightweightBackgroundPriority.UI) + private val uiHelperExecutor: ListeningExecutorService, + private val activityManagerWrapper: ActivityManagerWrapper, + private val msdlPlayerWrapper: MSDLPlayerWrapper, +) { + @AssistedFactory + interface Factory { + fun create(recentsView: RecentsView<*, *>): RecentsDismissUtils + } + + /** + * [OnDeskAddedListener] which launches the new desk right after it is created. + * + * This is mainly used for clearing all desks via the clear all button in the recent view or the + * removal of the last task in a desk. + */ + private val launchNewDeskListener = + object : OnDeskAddedListener { + override fun onDeskAdded(desktopTaskView: DesktopTaskView) { + desktopTaskView.launchWithAnimation() + recentsView.mUtils.removeOnDeskAddedListener(this) + } + } + + /** + * Runs the default spring animation when a dismissed task view in overview is released. + * + *

When a task dismiss is cancelled, the task will return to its original position via a + * spring animation. As it passes the threshold of its settling state, its neighbors will spring + * in response to the perceived impact of the settling task. + */ + fun createTaskDismissSpringAnimation( + dismissedTaskView: TaskView?, + shouldRemoveTaskView: Boolean, + isSplitSelection: Boolean, + ): SpringSet? { + if (dismissedTaskView == null || isSplitSelection) { + return createTaskDismissSpringAnimation( + dismissedTaskView, + isDismissing = true, + DismissedTaskData( + startVelocity = 0f, + dismissLength = 0, + dismissThreshold = 0, + finalPosition = 0f, + ), + shouldRemoveTaskView, + isSplitSelection, + ) + } + return createTaskDismissSpringAnimation( + dismissedTaskView, + isDismissing = true, + getDefaultDismissedTaskData(dismissedTaskView), + shouldRemoveTaskView, + isSplitSelection, + ) + } + + /** + * Runs the spring animations when a dismissed task view in overview is released. + * + *

When a task dismiss is cancelled, the task will return to its original position via a + * spring animation. As it passes the threshold of its settling state, its neighbors will spring + * in response to the perceived impact of the settling task. + */ + fun createTaskDismissSpringAnimation( + dismissedTaskView: TaskView?, + isDismissing: Boolean, + dismissedTaskData: DismissedTaskData, + shouldRemoveTaskView: Boolean, + isSplitSelection: Boolean, + ): SpringSet? { + val gridEndData = getGridEndData(dismissedTaskView) + val dismissedTaskSecondaryDimension = + if (dismissedTaskView == null) + recentsView.pagedOrientationHandler.getSecondarySize( + recentsView.mLastComputedTaskSize.toRectF() + ) + else { + recentsView.pagedOrientationHandler + .getSecondaryDimension(dismissedTaskView) + .toFloat() + } + val verticalFactor = + recentsView.pagedOrientationHandler.getTaskDismissVerticalDirection().toFloat() + val startVelocity = + abs(dismissedTaskData.startVelocity).coerceAtLeast(dismissedTaskSecondaryDimension) * + dismissedTaskData.startVelocity.sign + + // Spring that animates the dismissed task. + val dismissedTaskViewSpring = + if (isSplitSelection || dismissedTaskView == null) null + else { + createDismissedTaskViewSpringAnimation( + dismissedTaskView, + isDismissing, + DismissedTaskData( + startVelocity = startVelocity, + dismissLength = dismissedTaskData.dismissLength, + finalPosition = dismissedTaskData.finalPosition, + dismissThreshold = dismissedTaskData.dismissThreshold, + ), + ) + } + + // SpringSet tracking all dismiss springs before running end-snapping and relayout. + var springSet = + dismissedTaskViewSpring?.let { SpringSet(it, dismissedTaskData.finalPosition) } + + if (isDismissing) { + // The spring set that will reflow the tasks to fill the gap left by the dismissed task. + val reflowSpringSet = + createTaskGridReflowSpringSet( + dismissedTaskView, + getDismissedTaskGapForReflow(dismissedTaskView, isSplitSelection), + gridEndData, + isSplitSelection, + ) + if (springSet == null) { + // Only reflow, as there is no dismissed task to animate. + springSet = reflowSpringSet + } else if (reflowSpringSet != null) { + springSet.playAfterThreshold( + driverThreshold = dismissedTaskSecondaryDimension * verticalFactor, + triggeredSpringSet = reflowSpringSet, + ) + } + } else if (springSet != null && dismissedTaskView != null) { + // Neighbor settling spring animations. + val neighborSettlingSpringSet = + createNeighborSettlingSpringSet(dismissedTaskView, isSpringDirectionVertical = true) + springSet.playAfterThreshold( + driverThreshold = dismissedTaskData.finalPosition, + triggeredSpringSet = neighborSettlingSpringSet, + minVelocity = startVelocity, + ) + springSet.addEndListener { + InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS) + } + } + + if (!isSplitSelection) { + springSet?.addStartListener { + InteractionJankMonitorWrapper.begin( + recentsView, + Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS, + ) + } + } + val endRunnable = { + if (isDismissing) { + onEndSnappingAndRelayout( + dismissedTaskView, + shouldRemoveTaskView, + isSplitSelection, + gridEndData, + ) + } else { + recentsView.onDismissAnimationEnds() + } + } + if (springSet == null) { + endRunnable() + return null + } + return springSet.addEndListener(endRunnable).start() + } + + /** Default dismissed task view spring animation. */ + private fun createDismissedTaskViewSpringAnimation( + dismissedTaskView: TaskView + ): SpringAnimation? { + return createDismissedTaskViewSpringAnimation( + dismissedTaskView, + isDismissing = true, + getDefaultDismissedTaskData(dismissedTaskView), + ) + } + + /** Dismissed task view spring animation. */ + private fun createDismissedTaskViewSpringAnimation( + dismissedTaskView: TaskView, + isDismissing: Boolean, + dismissedTaskData: DismissedTaskData, + ): SpringAnimation? { + val taskDismissFloatProperty = + FloatPropertyCompat.createFloatPropertyCompat( + dismissedTaskView.secondaryDismissTranslationProperty + ) + var previousDisplacement = taskDismissFloatProperty.getValue(dismissedTaskView) + // Animate dismissed task towards dismissal or rest state. + val dismissedTaskViewSpringAnimation = + SpringAnimation(dismissedTaskView, taskDismissFloatProperty) + .setSpring( + createExpressiveDismissSpringForce() + .setFinalPosition(dismissedTaskData.finalPosition) + ) + .setStartVelocity(dismissedTaskData.startVelocity) + .addUpdateListener { animation, currentDisplacement, _ -> + // Play haptic as task crosses dismiss threshold from above or below. + val previousBeyondThreshold = + abs(previousDisplacement) >= abs(dismissedTaskData.dismissThreshold) + val currentBeyondThreshold = + abs(currentDisplacement) >= abs(dismissedTaskData.dismissThreshold) + if (previousBeyondThreshold != currentBeyondThreshold) { + msdlPlayerWrapper.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR) + } + previousDisplacement = currentDisplacement + + if (dismissedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) { + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value = + taskDismissFloatProperty.getValue(dismissedTaskView) + } + recentsView.redrawLiveTile() + } + // End dismissed task animation once beyond the screen so next animations play. + if ( + isDismissing && + abs(currentDisplacement) >= abs(dismissedTaskData.dismissLength) + ) { + (animation as SpringAnimation).skipToEnd() + } + } + return dismissedTaskViewSpringAnimation + } + + private fun getDefaultDismissedTaskData(dismissedTaskView: TaskView): DismissedTaskData { + with(recentsView) { + dismissedTaskView.getThumbnailBounds(mTempRect, /* relativeToDragLayer= */ true) + val secondaryLayerDimension = + pagedOrientationHandler.getSecondaryDimension( + (mContainer as ActivityContext).getDragLayer() + ) + val verticalFactor = pagedOrientationHandler.getTaskDismissVerticalDirection().toFloat() + val dismissLength = + (pagedOrientationHandler.getTaskDismissLength(secondaryLayerDimension, mTempRect) * + verticalFactor) + .toInt() + val dismissThreshold = (dismissLength * DEFAULT_DISMISS_THRESHOLD_FRACTION).toInt() + val startVelocity = mTempRect.height().toFloat() + val finalPosition = dismissLength.toFloat() + return DismissedTaskData( + startVelocity = startVelocity, + dismissLength = dismissLength, + finalPosition = finalPosition, + dismissThreshold = dismissThreshold, + ) + } + } + + /** Dismisses all */ + fun dismissAllTasks() { + val allDismissSprings = + recentsView.mUtils.taskViews + .reversed() + .filter { taskView -> recentsView.isTaskViewVisible(taskView) } + .mapNotNull { createDismissedTaskViewSpringAnimation(it) } + SpringSet(SpringAnimation(FloatValueHolder()).setSpring(SpringForce(1f))) + .playTogether(allDismissSprings) + .addEndListener { + with(recentsView) { + // Remove desktops first, since desks can be empty (so they have no recent + // tasks), and closing all tasks on a desk doesn't always necessarily mean that + // the desk will be removed. So, there are no guarantees that the below call to + // `ActivityManagerWrapper::removeAllRecentTasks()` will be enough. + if (areMultiDesksFlagsEnabled() && context.displayId.isExternalDisplay) { + mUtils.addOnDeskAddedListener(launchNewDeskListener) + } + systemUiProxy.removeAllDesks() + + // Remove all the task views now + finishRecentsAnimation(/* toRecents */ true, /* shouldPip */ false) { + uiHelperExecutor.execute { activityManagerWrapper.removeAllRecentTasks() } + removeAllTaskViews() + if (context.displayId.isDefaultDisplay || !areMultiDesksFlagsEnabled()) { + startHome() + } + } + } + } + .start() + } + + /** Bounce neighboring tasks due to a canceled dismiss or the reflow of tasks after dismiss. */ + private fun createNeighborSettlingSpringSet( + dismissedTaskView: TaskView, + tasksToExclude: List = emptyList(), + isSpringDirectionVertical: Boolean, + ): SpringSet { + // Empty spring animation exists for conditional start, and to drive neighboring springs. + val neighborsToSettle = + SpringAnimation(FloatValueHolder()) + .setSpring(createExpressiveDismissSpringForce().setFinalPosition(0f)) + val neighborSettlingSpringSet = SpringSet(neighborsToSettle) + + // Add tasks before dismissed index, fanning out from the dismissed task. + // The order they are added matters, as each spring drives the next. + var previousNeighbor = neighborsToSettle + getTasksOffsetPairAdjacentToDismissedTask(dismissedTaskView, towardsStart = true) + .filter { (taskView, _) -> !tasksToExclude.contains(taskView) } + .forEach { (taskView, offset) -> + previousNeighbor = + createNeighboringTaskViewSpringAnimation( + taskView, + offset * ADDITIONAL_DISMISS_DAMPING_RATIO, + previousNeighbor, + isSpringDirectionVertical, + neighborSettlingSpringSet, + ) + } + // Add tasks after dismissed index, fanning out from the dismissed task. + // The order they are added matters, as each spring drives the next. + previousNeighbor = neighborsToSettle + getTasksOffsetPairAdjacentToDismissedTask(dismissedTaskView, towardsStart = false) + .filter { (taskView, _) -> !tasksToExclude.contains(taskView) } + .forEach { (taskView, offset) -> + previousNeighbor = + createNeighboringTaskViewSpringAnimation( + taskView, + offset * ADDITIONAL_DISMISS_DAMPING_RATIO, + previousNeighbor, + isSpringDirectionVertical, + neighborSettlingSpringSet, + ) + } + return neighborSettlingSpringSet + } + + /** + * Gets pairs of (TaskView, offset) adjacent the dismissed task in visual order. + * + *

Gets tasks either before or after the dismissed task along with their offset from it. The + * offset is the distance between indices for carousels, or distance between columns for grids. + */ + private fun getTasksOffsetPairAdjacentToDismissedTask( + dismissedTaskView: TaskView, + towardsStart: Boolean, + ): Sequence> { + if (recentsView.showAsGrid()) { + val taskGridNavHelper = + TaskGridNavHelper( + recentsView.mUtils.getTopRowIdArray(), + recentsView.mUtils.getBottomRowIdArray(), + recentsView.mUtils.getLargeTaskViewIds(), + hasAddDesktopButton = false, + ) + return taskGridNavHelper + .gridTaskViewIdOffsetPairInTabOrderSequence( + dismissedTaskView.taskViewId, + towardsStart, + ) + .mapNotNull { (taskViewId, columnOffset) -> + recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView -> + Pair(taskView, columnOffset) + } + } + } else { + val taskViewList = recentsView.mUtils.taskViews.toList() + val dismissedTaskViewIndex = taskViewList.indexOf(dismissedTaskView) + if (taskViewList.isEmpty() || dismissedTaskViewIndex == -1) return emptySequence() + + return if (towardsStart) { + taskViewList + .take(dismissedTaskViewIndex) + .reversed() + .mapIndexed { index, taskView -> Pair(taskView, index + 1) } + .asSequence() + } else { + taskViewList + .takeLast(taskViewList.size - dismissedTaskViewIndex - 1) + .mapIndexed { index, taskView -> Pair(taskView, index + 1) } + .asSequence() + } + } + } + + /** Creates a neighboring task view spring, driven by the spring of its neighbor. */ + private fun createNeighboringTaskViewSpringAnimation( + taskView: TaskView, + dampingOffsetRatio: Float, + previousNeighborSpringAnimation: SpringAnimation, + springingDirectionVertical: Boolean, + neighborSettlingSpringSet: SpringSet, + ): SpringAnimation { + val springProperty = + if (springingDirectionVertical) taskView.secondaryDismissTranslationProperty + else taskView.primaryDismissTranslationProperty + val neighboringTaskViewSpringAnimation = + SpringAnimation(taskView, FloatPropertyCompat.createFloatPropertyCompat(springProperty)) + .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio)) + // Update live tile on spring animation. + if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) { + neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ -> + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + val taskTranslation = + if (springingDirectionVertical) { + remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation + } else { + remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation + } + taskTranslation.value = springProperty.get(taskView) + } + recentsView.redrawLiveTile() + } + } + neighborSettlingSpringSet.trackSpring(neighboringTaskViewSpringAnimation) + // Drive current neighbor's spring with the previous neighbor's. + previousNeighborSpringAnimation.addUpdateListener { _, value, _ -> + neighboringTaskViewSpringAnimation.animateToFinalPosition(value) + } + return neighboringTaskViewSpringAnimation + } + + /** Animates with springs the TaskViews beyond the dismissed task to fill the gap it left. */ + private fun createTaskGridReflowSpringSet( + dismissedTaskView: TaskView?, + dismissedTaskGap: Float, + gridEndData: GridEndData, + isSplitSelection: Boolean, + ): SpringSet? { + val towardsStart = if (recentsView.isRtl) dismissedTaskGap < 0 else dismissedTaskGap > 0 + // Grid end translation to run after all reflow animations have completed. + val gridEndSpringSet = createGridEndTranslationSpringSet(gridEndData) + val tasksWithOffsetsToReflow = getTasksToReflow(dismissedTaskView, towardsStart) + if (tasksWithOffsetsToReflow.isEmpty()) { + return gridEndSpringSet + } else { + // Empty spring exists for conditional start, and to drive neighboring springs. + val reflowSpringAnimationDriver = + SpringAnimation(FloatValueHolder()) + .setSpring( + createExpressiveGridReflowSpringForce(finalPosition = dismissedTaskGap) + ) + recentsView.mTaskViewsDismissPrimaryTranslations.clear() + // Separate spring end manager for reflow to coordinate start of grid end springs. + val reflowSpringSet = SpringSet(reflowSpringAnimationDriver, dismissedTaskGap) + buildDismissReflowSpringAnimationChain( + tasksWithOffsetsToReflow, + dismissedTaskGap, + previousSpring = reflowSpringAnimationDriver, + reflowSpringSet, + isSplitSelection, + ) + + // Animate the settling of the neighbors as reflow tasks settle into place. + if (dismissedTaskView != null) { + val neighborSettlingSpringSet = + createNeighborSettlingSpringSet( + dismissedTaskView, + tasksToExclude = tasksWithOffsetsToReflow.map { (taskView, _) -> taskView }, + isSpringDirectionVertical = false, + ) + reflowSpringSet.playAfterThreshold( + driverThreshold = dismissedTaskGap, + triggeredSpringSet = neighborSettlingSpringSet, + ) + } + if (gridEndSpringSet != null) { + reflowSpringSet.playAfterThreshold( + driverThreshold = dismissedTaskGap, + triggeredSpringSet = gridEndSpringSet, + ) + } + return reflowSpringSet + } + } + + private fun getDismissedTaskGapForReflow( + dismissedTaskView: TaskView?, + isSplitSelection: Boolean, + ): Float { + with(recentsView) { + val dismissedTaskGap = + if (dismissedTaskView == null) { + 0f + } else { + // If current page beyond last TaskView's index, use last TaskView to calculate + // offset. + val lastTaskViewIndex = indexOfChild(mUtils.getLastTaskView()) + val currentPage = currentPage.coerceAtMost(lastTaskViewIndex) + val dismissHorizontalFactor = + when { + dismissedTaskView.isGridTask -> 1f + currentPage == lastTaskViewIndex -> -1f + indexOfChild(dismissedTaskView) < currentPage -> -1f + else -> 1f + } * (if (isRtl) 1f else -1f) + (pagedOrientationHandler.getPrimarySize(dismissedTaskView) + pageSpacing) * + dismissHorizontalFactor + } + // Sliding translation for splitting tasks with large tiles present. + val slidingTranslation = + if (isSplitSelection && currentPageTaskView is DesktopTaskView) { + val nextSnappedPage = indexOfChild(mUtils.getFirstNonDesktopTaskView()) + val newClearAllShortTotalWidthTranslation = + getGridEndData(dismissedTaskView = null) + .newClearAllShortTotalWidthTranslation + pagedOrientationHandler.getPrimaryScroll(this) - + getScrollForPage(nextSnappedPage) + + if (isRtl) newClearAllShortTotalWidthTranslation + else -newClearAllShortTotalWidthTranslation + } else { + 0f + } + return dismissedTaskGap + if (isRtl) slidingTranslation else -slidingTranslation + } + } + + private fun getTasksToReflow( + dismissedTaskView: TaskView?, + towardsStart: Boolean, + ): List> { + // Null if splitting tasks while Desktop tasks are visible. Reflow all remaining grid tasks. + if (dismissedTaskView == null) { + return (recentsView.mUtils.getTopRowTaskViews().mapIndexed { index, taskView -> + taskView to index + } + + recentsView.mUtils.getBottomRowTaskViews().mapIndexed { index, taskView -> + taskView to index + }) + .sortedBy { it.second } + } + val isDismissedTaskViewOnTopRow = recentsView.isOnGridTopRow(dismissedTaskView) + val isDismissedTaskViewOnBottomRow = recentsView.isOnGridBottomRow(dismissedTaskView) + return getTasksOffsetPairAdjacentToDismissedTask(dismissedTaskView, towardsStart) + .filter { (taskView, _) -> + when { + isDismissedTaskViewOnBottomRow -> recentsView.isOnGridBottomRow(taskView) + isDismissedTaskViewOnTopRow -> recentsView.isOnGridTopRow(taskView) + else -> true + } + } + .toList() + } + + private fun willTaskBeVisibleAfterDismiss(taskView: TaskView, taskTranslation: Int): Boolean { + val screenStart = recentsView.pagedOrientationHandler.getPrimaryScroll(recentsView) + val screenEnd = + screenStart + recentsView.pagedOrientationHandler.getMeasuredSize(recentsView) + return recentsView.isTaskViewWithinBounds( + taskView, + screenStart, + screenEnd, + /* taskViewTranslation = */ taskTranslation, + ) + } + + /** Builds a chain of spring animations for task reflow after dismissal */ + private fun buildDismissReflowSpringAnimationChain( + taskViewOffsetPairs: List>, + dismissedTaskGap: Float, + previousSpring: SpringAnimation, + reflowSpringSet: SpringSet, + isSplitSelection: Boolean, + ): SpringAnimation { + if (taskViewOffsetPairs.isEmpty()) return previousSpring + var lastTaskViewSpring = previousSpring + var previousColumnDriverSpring = previousSpring + var lastColumnOffset = taskViewOffsetPairs.first().second + taskViewOffsetPairs + .filter { (taskView, _) -> + willTaskBeVisibleAfterDismiss(taskView, dismissedTaskGap.roundToInt()) + } + .forEach { (taskView, column) -> + val startValue = + if ( + isSplitSelection && + taskView !is DesktopTaskView && + recentsView.currentPageTaskView is DesktopTaskView && + !recentsView.isTaskViewVisible(taskView) + ) { + dismissedTaskGap + + (if (recentsView.isRtl) -recentsView.mLastComputedTaskSize.right + else recentsView.mLastComputedTaskSize.right) + } else 0f + val taskViewSpringAnimation = + SpringAnimation( + taskView, + FloatPropertyCompat.createFloatPropertyCompat( + taskView.primaryDismissTranslationProperty + ), + ) + .setSpring(createExpressiveGridReflowSpringForce(dismissedTaskGap)) + .setStartValue(startValue) + // Update live tile on spring animation. + if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) { + taskViewSpringAnimation.addUpdateListener { _, _, _ -> + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation.value = + taskView.primaryDismissTranslationProperty.get(taskView) + } + recentsView.redrawLiveTile() + } + } + // For grid overview, if we are animating tasks in the same column offset, they + // should both be pulled by the previous spring at the same time. + if (column != lastColumnOffset) { + previousColumnDriverSpring = lastTaskViewSpring + lastColumnOffset = column + } + previousColumnDriverSpring.addUpdateListener { _, value, _ -> + taskViewSpringAnimation.animateToFinalPosition(value) + } + lastTaskViewSpring = taskViewSpringAnimation + reflowSpringSet.trackSpring(taskViewSpringAnimation, dismissedTaskGap) + recentsView.mTaskViewsDismissPrimaryTranslations[taskView] = + dismissedTaskGap.toInt() + } + return lastTaskViewSpring + } + + /** Animates the grid to compensate the clear all gap after dismissal. */ + private fun createGridEndTranslationSpringSet(gridEndData: GridEndData): SpringSet? { + val gridEndOffset = gridEndData.gridEndOffset + if (gridEndOffset == 0f) { + return null + } + + // Create spring animation to drive all task view grid translations simultaneously. + val gridEndSpring = + SpringAnimation(FloatValueHolder()) + .setSpring(createExpressiveGridReflowSpringForce(finalPosition = gridEndOffset)) + val gridEndSpringSet = SpringSet(gridEndSpring, gridEndOffset) + recentsView.mUtils.taskViews.forEach { taskView -> + val taskViewGridEndSpringAnimation = + SpringAnimation( + taskView, + FloatPropertyCompat.createFloatPropertyCompat(GRID_END_TRANSLATION_X), + ) + .setSpring(createExpressiveGridReflowSpringForce(gridEndOffset)) + // Update live tile on spring animation. + if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) { + taskViewGridEndSpringAnimation.addUpdateListener { _, _, _ -> + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + remoteTargetHandle.taskViewSimulator.taskPrimaryTranslation.value = + GRID_END_TRANSLATION_X.get(taskView) + } + recentsView.redrawLiveTile() + } + } + gridEndSpringSet.trackSpring(taskViewGridEndSpringAnimation, gridEndOffset) + gridEndSpring.addUpdateListener { _, value, _ -> + taskViewGridEndSpringAnimation.animateToFinalPosition(value) + } + } + // Animate alpha of clear all if translating grid to hide it. + if (recentsView.isClearAllHidden) { + SpringAnimation( + recentsView.clearAllButton, + FloatPropertyCompat.createFloatPropertyCompat(ClearAllButton.DISMISS_ALPHA), + ) + .setSpring(createExpressiveDismissAlphaSpringForce()) + .addEndListener { _, _, _, _ -> recentsView.clearAllButton.dismissAlpha = 1f } + .animateToFinalPosition(0f) + } + return gridEndSpringSet + } + + /** Returns the distance between the end of the grid and clear all button after dismissal. */ + fun getGridEndData( + dismissedTaskView: TaskView?, + isExpressiveDismiss: Boolean = true, + isFocusedTaskDismissed: Boolean = false, + nextFocusedTaskView: TaskView? = null, + isStagingFocusedTask: Boolean = false, + nextFocusedTaskFromTop: Boolean = false, + nextFocusedTaskWidth: Float = 0f, + ): GridEndData { + var gridEndOffset = 0f + var snapToLastTask = false + var newClearAllShortTotalWidthTranslation: Float + var currentPageSnapsToEndOfGrid: Boolean + with(recentsView) { + val lastGridTaskView = if (showAsGrid()) lastGridTaskView else null + val currentPageScroll = getScrollForPage(currentPage) + val lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView)) + currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll + var topGridRowCount = mTopRowIdSet.size() + var bottomGridRowCount = + taskViewCount - mTopRowIdSet.size() - mUtils.getLargeTileCount() + val topRowLonger = topGridRowCount > bottomGridRowCount + val bottomRowLonger = bottomGridRowCount > topGridRowCount + val dismissedFromTop = + dismissedTaskView != null && mTopRowIdSet.contains(dismissedTaskView.taskViewId) + val dismissedFromBottom = + dismissedTaskView != null && !dismissedFromTop && !dismissedTaskView.isLargeTile + if (dismissedFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) { + topGridRowCount-- + } + if (dismissedFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) { + bottomGridRowCount-- + } + newClearAllShortTotalWidthTranslation = + getNewClearAllShortTotalWidthTranslation( + topGridRowCount, + bottomGridRowCount, + isStagingFocusedTask, + ) + val isLastGridTaskViewVisibleForDismiss = + when { + lastGridTaskView == null -> false + isExpressiveDismiss -> + isTaskViewVisible(lastGridTaskView) || lastGridTaskView == dismissedTaskView + else -> lastGridTaskView.isVisibleToUser + } + if (!isLastGridTaskViewVisibleForDismiss) { + return GridEndData( + gridEndOffset, + snapToLastTask, + newClearAllShortTotalWidthTranslation, + currentPageSnapsToEndOfGrid, + ) + } + val dismissedTaskWidth = + if (dismissedTaskView == null) 0f + else (dismissedTaskView.layoutParams.width + pageSpacing).toFloat() + val gapWidth = + when { + (topRowLonger && dismissedFromTop) || + (bottomRowLonger && dismissedFromBottom) -> dismissedTaskWidth + nextFocusedTaskView != null && + ((topRowLonger && nextFocusedTaskFromTop) || + (bottomRowLonger && !nextFocusedTaskFromTop)) -> nextFocusedTaskWidth + else -> 0f + } + if (gapWidth > 0) { + if (clearAllShortTotalWidthTranslation == 0) { + val gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation + gridEndOffset += if (isRtl) -gapCompensation else gapCompensation + } + if (isClearAllHidden) { + // If ClearAllButton isn't fully shown, snap to the last task. + snapToLastTask = true + } + } + val isLeftRightSplit = + (mContainer as ActivityContext).getDeviceProfile().isLeftRightSplit && + isSplitSelectionActive + if (isLeftRightSplit && !isStagingFocusedTask) { + // LastTask's scroll is the minimum scroll in split select, if current scroll is + // beyond that, we'll need to snap to last task instead. + getLastGridTaskView()?.let { lastTask -> + val primaryScroll = pagedOrientationHandler.getPrimaryScroll(this) + val lastTaskScroll = getScrollForPage(indexOfChild(lastTask)) + if ( + (isRtl && primaryScroll < lastTaskScroll) || + (!isRtl && primaryScroll > lastTaskScroll) + ) { + snapToLastTask = true + } + } + } + if (snapToLastTask) { + gridEndOffset += snapToLastTaskScrollDiff.toFloat() + } else if (isLeftRightSplit && currentPageSnapsToEndOfGrid) { + // Use last task as reference point for scroll diff and snapping calculation as it's + // the only invariant point in landscape split screen. + snapToLastTask = true + } + + // Handle large tile scroll when dismissing the last small task. + if (mUtils.getGridTaskCount() == 1 && dismissedTaskView?.isGridTask == true) { + mUtils.getLastLargeTaskView()?.let { lastLargeTile -> + val primaryScroll = pagedOrientationHandler.getPrimaryScroll(this) + val lastLargeTileScroll = getScrollForPage(indexOfChild(lastLargeTile)) + gridEndOffset = (primaryScroll - lastLargeTileScroll).toFloat() + + if (!isClearAllHidden) { + // If ClearAllButton is visible, reduce the distance by scroll difference + // between ClearAllButton and the last task. + gridEndOffset += + getLastTaskScroll( + /*clearAllScroll=*/ 0, + pagedOrientationHandler.getPrimarySize(clearAllButton), + ) + .toFloat() + } + } + } + } + return GridEndData( + gridEndOffset, + snapToLastTask, + newClearAllShortTotalWidthTranslation, + currentPageSnapsToEndOfGrid, + ) + } + + private fun getNewClearAllShortTotalWidthTranslation( + topGridRowCount: Int, + bottomGridRowCount: Int, + isStagingFocusedTask: Boolean, + ): Float { + with(recentsView) { + if (clearAllShortTotalWidthTranslation != 0) { + return 0f + } + // If first task is not in the expected position (mLastComputedTaskSize) and too + // close to ClearAllButton, then apply extra translation to ClearAllButton. + var longRowWidth = + max(topGridRowCount, bottomGridRowCount) * + (mLastComputedGridTaskSize.width() + pageSpacing) + if (!enableGridOnlyOverview() && !isStagingFocusedTask) { + longRowWidth += mLastComputedTaskSize.width() + pageSpacing + } + val firstTaskStart = mLastComputedGridSize.left + longRowWidth + val expectedFirstTaskStart = mLastComputedTaskSize.right + // Compensate the removed gap if we don't already have shortTotalCompensation, + // and adjust accordingly to the new shortTotalCompensation after dismiss. + return if (firstTaskStart < expectedFirstTaskStart) { + (expectedFirstTaskStart - firstTaskStart).toFloat() + } else { + 0f + } + } + } + + private fun onEndSnappingAndRelayout( + dismissedTaskView: TaskView?, + shouldRemoveTask: Boolean, + dismissingForSplitSelection: Boolean, + gridEndData: GridEndData, + ) { + with(recentsView) { + if (pageCount == 0) { + return@with + } + updateCurveProperties() + loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL) + + // Page snapping and relayout to run after all animations have completed. + val onFinishComplete = { + // Reset task translations as they may have updated via the dismiss animations. + resetTaskVisuals() + + // Denote if any task has been dismissed for grid rebalancing. + mAnyTaskHasBeenDismissed = true + // Cache group task before removing. + handleGroupTaskRemoval(dismissedTaskView, shouldRemoveTask) + + // Get page to snap to before removing dismissed task. + val dismissedTaskViewId = dismissedTaskView?.taskViewId ?: INVALID_TASK_ID + val pageToSnapTo = + when { + (dismissedTaskView != null && + (!showAsGrid() || dismissedTaskView.isLargeTile)) -> { + getPageToSnapTo(dismissedTaskView) + } + showAsGrid() -> { + getPageToSnapToForGrid(gridEndData, dismissedTaskViewId) + } + else -> { + currentPage + } + } + + // Remove dismissed task. + removeViewInLayout(dismissedTaskView) + mTopRowIdSet.remove(dismissedTaskViewId) + + // Update the UI after removal and snap to page. + updateUiAfterTaskRemoval(dismissedTaskView, pageToSnapTo) + + if (!dismissingForSplitSelection) { + InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS) + } + } + + // Run the final page snapping and relayout + if (enableDrawingLiveTile && dismissedTaskView?.isRunningTask == true) { + finishRecentsAnimation( + /* toRecents */ true, + /* shouldPip */ false, + onFinishComplete, + ) + } else { + onFinishComplete() + } + } + } + + /** + * Caches the groupTask before removing it. As the deskId might become invalid by + * removeViewInLayout called on the dismissed task. It might happen before + * removeGroupTaskInternal which runs on a helper thread. + */ + private fun handleGroupTaskRemoval(dismissedTaskView: TaskView?, shouldRemoveTask: Boolean) { + with(recentsView) { + if (shouldRemoveTask && dismissedTaskView != null) { + val groupTask = dismissedTaskView.groupTask + if (groupTask != null) { + // For the multi desk case, the launcher should switch to the new desk once the + // last task of the previous desk is removed. + if ( + areMultiDesksFlagsEnabled() && + context.displayId.isExternalDisplay && + taskViewCount == 1 && + contains(dismissedTaskView) + ) { + mUtils.addOnDeskAddedListener(launchNewDeskListener) + } + if (dismissedTaskView.isRunningTask) { + finishRecentsAnimation(/* toRecents */ true, /* shouldPip */ false) { + removeGroupTaskInternal(groupTask) + } + } else { + removeGroupTaskInternal(groupTask) + } + (mContainer as ActivityContext) + .statsLogManager + .logger() + .withItemInfo(dismissedTaskView.itemInfo) + .log(LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP) + } + } + } + } + + private fun getPageToSnapTo(dismissedTaskView: TaskView?): Int { + with(recentsView) { + var pageToSnapTo = currentPage + if ( + indexOfChild(dismissedTaskView) < pageToSnapTo || + pageToSnapTo == indexOfChild(mUtils.getLastTaskView()) + ) { + pageToSnapTo-- + } + return pageToSnapTo + } + } + + private fun getPageToSnapToForGrid(gridEndData: GridEndData, dismissedTaskViewId: Int): Int { + with(recentsView) { + var pageToSnapTo = currentPage + var taskViewIdToSnapTo = -1 + currentPageScrollDiff = 0 + + if (gridEndData.gridEndOffset != 0f) { + if (gridEndData.snapToLastTask) { + // Last task will be determined after removing dismissed task. + pageToSnapTo = -1 + } else if (taskViewCount > 2) { + pageToSnapTo = indexOfChild(clearAllButton) + } else if (isClearAllHidden) { + // Snap to focused task if clear all is hidden. + pageToSnapTo = firstTaskViewIndex + } + } else { + val snappedTaskView = currentPageTaskView + if (snappedTaskView != null && !gridEndData.snapToLastTask) { + val snappedTaskViewId = snappedTaskView.taskViewId + val isSnappedTaskInTopRow = mTopRowIdSet.contains(snappedTaskViewId) + val taskViewIdArray = + if (isSnappedTaskInTopRow) { + mUtils.getTopRowIdArray() + } else { + mUtils.getBottomRowIdArray() + } + val snappedIndex = taskViewIdArray.indexOf(snappedTaskViewId) + taskViewIdArray.removeValue(dismissedTaskViewId) + if (snappedIndex >= 0 && snappedIndex < taskViewIdArray.size()) { + taskViewIdToSnapTo = taskViewIdArray[snappedIndex] + } else if (snappedIndex == taskViewIdArray.size()) { + val inverseRowTaskViewIdArray = + if (isSnappedTaskInTopRow) { + mUtils.getBottomRowIdArray() + } else { + mUtils.getTopRowIdArray() + } + if (snappedIndex < inverseRowTaskViewIdArray.size()) { + taskViewIdToSnapTo = inverseRowTaskViewIdArray[snappedIndex] + } + } + } + val primaryScroll = pagedOrientationHandler.getPrimaryScroll(this) + val currentPageScroll = getScrollForPage(currentPage) + currentPageScrollDiff = primaryScroll - currentPageScroll + } + + // Calculate page to snap to as if the dismissed task view is removed from the grid. + val topRowIdArray = mUtils.getTopRowIdArray() + val bottomRowIdArray = mUtils.getBottomRowIdArray() + topRowIdArray.removeValue(dismissedTaskViewId) + bottomRowIdArray.removeValue(dismissedTaskViewId) + val children = + children.filter { child -> child != getTaskViewFromTaskViewId(dismissedTaskViewId) } + if (gridEndData.snapToLastTask) { + val lastGridTaskView = getLastGridTaskView(topRowIdArray, bottomRowIdArray) + pageToSnapTo = + if (lastGridTaskView == null) PagedView.INVALID_PAGE + else children.indexOf(lastGridTaskView as View) + if (pageToSnapTo == PagedView.INVALID_PAGE) { + val lastLargeTaskView = mUtils.getLastLargeTaskView() + pageToSnapTo = + if (lastLargeTaskView == null) PagedView.INVALID_PAGE + else children.indexOf(lastLargeTaskView as View) + } + } else if (taskViewIdToSnapTo != -1) { + // If snapping to another page due to indices rearranging, find + // the new index after dismissal & rearrange using the TaskView ID. + pageToSnapTo = + children.indexOf(getTaskViewFromTaskViewId(taskViewIdToSnapTo) as View) + if (!gridEndData.currentPageSnapsToEndOfGrid) { + val dismissedTaskViewIndex = + indexOfChild(getTaskViewFromTaskViewId(dismissedTaskViewId)) + currentPageScrollDiff += + getOffsetFromScrollPosition( + pageToSnapTo, + topRowIdArray, + bottomRowIdArray, + dismissedTaskViewIndex, + ) + } + } + return pageToSnapTo + } + } + + private fun updateUiAfterTaskRemoval(dismissedTaskView: TaskView?, pageToSnapTo: Int) { + with(recentsView) { + if (taskViewCount == 0) { + if (!isSplitSelectionActive) { + removeViewInLayout(clearAllButton) + removeViewInLayout(addDeskButton) + if (dismissedTaskView === homeTaskView) { + updateEmptyMessage() + } else { + if (!areMultiDesksFlagsEnabled() || context.displayId.isDefaultDisplay) { + startHome() + } + } + } + } else { + updateTaskSize() + mUtils.updateChildTaskOrientations() + updateScrollSynchronously() + + val highestVisibleTaskView = getHighestVisibleTaskView() + if (showAsGrid() && highestVisibleTaskView != null) { + rebalanceTasksInGrid(highestVisibleTaskView) + } + pageBeginTransition() + currentPage = pageToSnapTo + dispatchScrollChanged() + updateActionsViewFocusedScroll() + if ( + isClearAllHidden && + !(mContainer as ActivityContext) + .getDeviceProfile() + .deviceProperties + .isTablet + ) { + actionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false) + } + } + updateCurrentTaskActionsVisibility() + onDismissAnimationEnds() + mTaskViewsDismissPrimaryTranslations.clear() + } + } + + private fun rebalanceTasksInGrid(highestVisibleTaskView: TaskView) { + with(recentsView) { + val screenStart = pagedOrientationHandler.getPrimaryScroll(this) + val taskStart = + (pagedOrientationHandler.getChildStart(highestVisibleTaskView) + + highestVisibleTaskView.getOffsetAdjustment(true).toInt()) + + val shouldRebalance = + if (isRtl) { + taskStart <= screenStart + pageSpacing + } else { + val screenEnd = (screenStart + pagedOrientationHandler.getMeasuredSize(this)) + val taskSize = + (pagedOrientationHandler.getMeasuredSize(highestVisibleTaskView) * + highestVisibleTaskView.getSizeAdjustment(false)) + .toInt() + val taskEnd = taskStart + taskSize + taskEnd >= screenEnd - pageSpacing + } + + if (shouldRebalance) { + updateGridProperties(highestVisibleTaskView) + updateScrollSynchronously() + } + } + } + + /** Animates RecentsView's scale to the provided value, using spring animations. */ + fun animateRecentsScale(scale: Float): SpringAnimation { + val resourceProvider = DynamicResource.provider(recentsView.mContainer) + val dampingRatio = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio) + val stiffness = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_stiffness) + + // Spring which sets the Recents scale on update. This is needed, as the SpringAnimation + // struggles to animate small values like changing recents scale from 0.9 to 1. So + // we animate over a larger range (e.g. 900 to 1000) and convert back to the required value. + // (This is instead of converting RECENTS_SCALE_PROPERTY to a FloatPropertyCompat and + // animating it directly via springs.) + val initialRecentsScaleSpringValue = + RECENTS_SCALE_SPRING_MULTIPLIER * RECENTS_SCALE_PROPERTY.get(recentsView) + return SpringAnimation(FloatValueHolder(initialRecentsScaleSpringValue)) + .setSpring( + SpringForce(initialRecentsScaleSpringValue) + .setDampingRatio(dampingRatio) + .setStiffness(stiffness) + ) + .addUpdateListener { _, value, _ -> + RECENTS_SCALE_PROPERTY.setValue( + recentsView, + value / RECENTS_SCALE_SPRING_MULTIPLIER, + ) + } + .apply { animateToFinalPosition(RECENTS_SCALE_SPRING_MULTIPLIER * scale) } + } + + private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce { + val resourceProvider = DynamicResource.provider(recentsView.mContainer) + return SpringForce() + .setDampingRatio( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) + + dampingRatioOffset + ) + .setStiffness( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness) + ) + } + + private fun createExpressiveGridReflowSpringForce( + finalPosition: Float = Float.MAX_VALUE + ): SpringForce { + val resourceProvider = DynamicResource.provider(recentsView.mContainer) + return SpringForce(finalPosition) + .setDampingRatio( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_damping_ratio) + ) + .setStiffness( + resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_x_stiffness) + ) + } + + private fun createExpressiveDismissAlphaSpringForce(): SpringForce { + val resourceProvider = DynamicResource.provider(recentsView.mContainer) + return SpringForce() + .setDampingRatio( + resourceProvider.getFloat(R.dimen.expressive_dismiss_effects_damping_ratio) + ) + .setStiffness(resourceProvider.getFloat(R.dimen.expressive_dismiss_effects_stiffness)) + } + + /** + * Plays a set of {@link SpringAnimation} objects in the specified order. + * + *

Animations can play together, in sequence, or after a specified threshold is passed. + */ + class SpringSet(private val driverSpring: SpringAnimation, driverSpringThreshold: Float = 0f) { + private val trackedSprings = mutableSetOf() + private val trackedSpringSets = mutableSetOf() + private var runningSpringCount = 0 + private val startListenerSet = mutableSetOf<() -> Unit>() + private val endListenerSet = mutableSetOf<() -> Unit>() + private var hasStarted = false + private var hasEnded = false + + init { + trackSpring(driverSpring, driverSpringThreshold) + } + + fun start(): SpringSet { + if (hasStarted) { + return this + } + hasStarted = true + if (trackedSprings.isEmpty()) { + onEnd() + return this + } + driverSpring.start() + startListenerSet.forEach { it() } + return this + } + + private fun onEnd() { + if (hasEnded) { + return + } + hasEnded = true + endListenerSet.forEach { it() } + } + + fun cancel(): SpringSet { + driverSpring.cancel() + trackedSprings.forEach { it.cancel() } + trackedSpringSets.forEach { it.cancel() } + onEnd() + return this + } + + /** + * Increase spring constants to force animations to end quickly. + * + *

A high stiffness applies more force to the object to bring it to its end value. A + * damping ratio of 1f is critically damped, and the object will return to equilibrium + * within the shortest amount of time. + */ + fun speedUpSpringsToEnd(): SpringSet { + trackedSprings.forEach { + it.spring.setStiffness(SPEED_UP_STIFFNESS).setDampingRatio(1f) + } + trackedSpringSets.forEach { it.speedUpSpringsToEnd() } + return this + } + + fun addStartListener(startListener: () -> Unit): SpringSet { + startListenerSet.add(startListener) + return this + } + + fun addEndListener(endRunnable: () -> Unit): SpringSet { + endListenerSet.add(endRunnable) + return this + } + + fun trackSpring(spring: SpringAnimation, minimumDistance: Float = 0f): SpringSet { + if (trackedSprings.contains(spring)) { + throw IllegalArgumentException("SpringSet already contains this spring.") + } + trackedSprings.add(spring) + runningSpringCount++ + var canSpringEnd = false + spring.addUpdateListener { _, value, _ -> + // Do not allow end listener to fire until we have passed the minimum distance. + if (!canSpringEnd && abs(value - minimumDistance) < spring.minimumVisibleChange) { + canSpringEnd = true + } + } + spring.addEndListener { _, _, _, _ -> + if (!canSpringEnd || runningSpringCount == 0) { + return@addEndListener + } + if (--runningSpringCount == 0) { + onEnd() + } + } + return this + } + + private fun trackSpringSet(springSet: SpringSet): SpringSet { + runningSpringCount++ + trackedSpringSets.add(springSet) + springSet.addEndListener { + if (--runningSpringCount == 0) { + onEnd() + } + } + return this + } + + fun playAfterThreshold( + driverThreshold: Float, + triggeredSpringSet: SpringSet, + minVelocity: Float = 0f, + ): SpringSet { + trackSpringSet(triggeredSpringSet) + var lastPosition = 0f + var isTriggered = false + driverSpring.addUpdateListener { _, value, velocity -> + // We do not compare to the threshold directly, as the update listener + // does not necessarily hit every value. Do not check again once it has started + // settling, as a spring can bounce past the end value multiple times. + if (isTriggered) return@addUpdateListener + if ( + lastPosition < driverThreshold && value >= driverThreshold || + lastPosition > driverThreshold && value <= driverThreshold + ) { + isTriggered = true + } + lastPosition = value + if (isTriggered) { + val startVelocity = + abs(velocity).coerceAtLeast(abs(minVelocity)) * velocity.sign + triggeredSpringSet.driverSpring.setStartVelocity(startVelocity) + triggeredSpringSet.start() + } + } + return this + } + + fun playTogether(springs: List): SpringSet { + springs.forEach { + trackSpring(it) + addStartListener { it.start() } + } + return this + } + } + + data class GridEndData( + val gridEndOffset: Float, + val snapToLastTask: Boolean, + val newClearAllShortTotalWidthTranslation: Float, + val currentPageSnapsToEndOfGrid: Boolean, + ) + + data class DismissedTaskData( + val startVelocity: Float, + val dismissLength: Int, + val finalPosition: Float, + val dismissThreshold: Int, + ) + + private companion object { + // The additional damping to apply to tasks further from the dismissed task. + private const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f + private const val RECENTS_SCALE_SPRING_MULTIPLIER = 1000f + private const val DEFAULT_DISMISS_THRESHOLD_FRACTION = 0.5f + private const val SPEED_UP_STIFFNESS = 100_000f + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 163732ff07..ae8aff3826 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -17,7 +17,8 @@ package com.android.quickstep.views; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.view.Surface.ROTATION_0; +import static android.os.Trace.traceBegin; +import static android.os.Trace.traceEnd; import static android.view.View.MeasureSpec.EXACTLY; import static android.view.View.MeasureSpec.makeMeasureSpec; @@ -25,21 +26,23 @@ import static com.android.app.animation.Interpolators.ACCELERATE; import static com.android.app.animation.Interpolators.ACCELERATE_0_75; import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; import static com.android.app.animation.Interpolators.DECELERATE_2; +import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; import static com.android.app.animation.Interpolators.FINAL_FRAME; import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.app.animation.Interpolators.OVERSHOOT_0_75; import static com.android.app.animation.Interpolators.clampToProgress; import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; -import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU; -import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; -import static com.android.launcher3.Flags.enableAdditionalHomeAnimations; -import static com.android.launcher3.Flags.enableGridOnlyOverview; +import static com.android.launcher3.Flags.enableCoroutineThreadingImprovements; +import static com.android.launcher3.Flags.enableDesktopExplodedView; +import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion; +import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile; +import static com.android.launcher3.Flags.enableOverviewBackgroundWallpaperBlur; import static com.android.launcher3.Flags.enableRefactorTaskThumbnail; import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; +import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; @@ -51,33 +54,31 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN; +import static com.android.launcher3.statehandlers.DesktopVisibilityController.INACTIVE_DESK_ID; import static com.android.launcher3.testing.shared.TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE; import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; +import static com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview; import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; +import static com.android.quickstep.BaseContainerInterface.getTaskDimension; import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; +import static com.android.quickstep.util.DesksUtils.areMultiDesksFlagsEnabled; import static com.android.quickstep.util.LogUtils.splitFailureMessage; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB; -import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP; import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS; -import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SCREEN; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE; +import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS; +import static com.android.quickstep.views.TaskView.SPLIT_ALPHA; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.animation.LayoutTransition; -import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; @@ -86,25 +87,28 @@ import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; import android.content.LocusId; +import android.content.pm.LauncherApps; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BlendMode; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.SystemClock; +import android.os.Trace; import android.os.UserHandle; import android.os.VibrationEffect; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; +import android.util.ArraySet; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Log; @@ -117,7 +121,6 @@ import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.View; import android.view.ViewDebug; -import android.view.ViewGroup; import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -125,20 +128,22 @@ import android.view.animation.Interpolator; import android.widget.ListView; import android.widget.OverScroller; import android.widget.Toast; +import android.window.DesktopModeFlags; import android.window.PictureInPictureSurfaceTransaction; +import android.window.TransitionInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.graphics.ColorUtils; +import androidx.dynamicanimation.animation.SpringAnimation; import com.android.internal.jank.Cuj; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener; +import com.android.launcher3.BuildConfig; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Flags; import com.android.launcher3.Insettable; -import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.PagedView; import com.android.launcher3.R; import com.android.launcher3.Utilities; @@ -149,11 +154,14 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.SpringProperty; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.dagger.LauncherComponentProvider; import com.android.launcher3.desktop.DesktopRecentsTransitionController; +import com.android.launcher3.deviceprofile.OverviewProfile; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.statehandlers.DepthController; +import com.android.launcher3.statehandlers.DesktopVisibilityController; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StatefulContainer; @@ -166,17 +174,19 @@ import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ResourceBasedOverride.Overrides; import com.android.launcher3.util.RunnableList; -import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; -import com.android.launcher3.util.Themes; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.TranslateEdgeEffect; import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.util.ViewPool; +import com.android.launcher3.util.coroutines.DispatcherProvider; +import com.android.launcher3.util.coroutines.ProductionDispatchers; import com.android.quickstep.BaseContainerInterface; import com.android.quickstep.GestureState; +import com.android.quickstep.HighResLoadingState; import com.android.quickstep.OverviewCommandHelper; +import com.android.quickstep.OverviewComponentObserver; import com.android.quickstep.RecentsAnimationController; import com.android.quickstep.RecentsAnimationTargets; import com.android.quickstep.RecentsFilterState; @@ -188,24 +198,34 @@ import com.android.quickstep.RotationTouchHelper; import com.android.quickstep.SplitSelectionListener; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskOverlayFactory; -import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.TaskViewUtils; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.ViewUtils; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.recents.data.TasksRepository; +import com.android.quickstep.recents.data.AppTimersRepository; +import com.android.quickstep.recents.data.AppTimersRepositoryImpl; +import com.android.quickstep.recents.data.RecentTasksRepository; +import com.android.quickstep.recents.data.RecentsDeviceProfileRepository; +import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl; +import com.android.quickstep.recents.data.RecentsRotationStateRepository; +import com.android.quickstep.recents.data.RecentsRotationStateRepositoryImpl; +import com.android.quickstep.recents.di.RecentsDependencies; import com.android.quickstep.recents.viewmodel.RecentsViewData; -import com.android.quickstep.util.ActiveGestureErrorDetector; -import com.android.quickstep.util.ActiveGestureLog; +import com.android.quickstep.recents.viewmodel.RecentsViewModel; +import com.android.quickstep.util.ActiveGestureProtoLogProxy; import com.android.quickstep.util.AnimUtils; import com.android.quickstep.util.DesktopTask; +import com.android.quickstep.util.FontUtils; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RecentsAtomicAnimationFactory; import com.android.quickstep.util.RecentsOrientedState; +import com.android.quickstep.util.SingleTask; import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps; import com.android.quickstep.util.SplitAnimationTimings; import com.android.quickstep.util.SplitSelectStateController; +import com.android.quickstep.util.SplitTask; import com.android.quickstep.util.SurfaceTransaction; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.TaskGridNavHelper; @@ -213,47 +233,57 @@ import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TaskVisualsChangeListener; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.VibrationConstants; -import com.android.quickstep.views.TaskView.TaskContainer; import com.android.systemui.plugins.ResourceProvider; import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.Task.TaskKey; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.common.pip.IPipAnimationListener; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.GroupedTaskInfo; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.pip.PipFlags; +import com.android.wm.shell.shared.split.SplitBounds; import kotlin.Unit; +import kotlin.collections.CollectionsKt; + +import kotlinx.coroutines.CoroutineScope; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Collectors; /** * A list of recent tasks. + * * @param : the container that should host recents view - * @param : the type of base state that will be used + * @param : the type of base state that will be used */ - -public abstract class RecentsView, STATE_TYPE extends BaseState> extends PagedView implements Insettable, - TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback, + HighResLoadingState.HighResLoadingStateChangedCallback, TaskVisualsChangeListener { - private static final String TAG = "RecentsView"; + protected static final String TAG = "RecentsView"; private static final boolean DEBUG = false; - public static final FloatProperty CONTENT_ALPHA = - new FloatProperty("contentAlpha") { + public static final FloatProperty> CONTENT_ALPHA = + new FloatProperty<>("contentAlpha") { @Override public void setValue(RecentsView view, float v) { view.setContentAlpha(v); @@ -265,8 +295,8 @@ public abstract class RecentsView FULLSCREEN_PROGRESS = - new FloatProperty("fullscreenProgress") { + public static final FloatProperty> FULLSCREEN_PROGRESS = + new FloatProperty<>("fullscreenProgress") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setFullscreenProgress(v); @@ -278,8 +308,8 @@ public abstract class RecentsView TASK_MODALNESS = - new FloatProperty("taskModalness") { + public static final FloatProperty> TASK_MODALNESS = + new FloatProperty<>("taskModalness") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskModalness(v); @@ -291,8 +321,8 @@ public abstract class RecentsView ADJACENT_PAGE_HORIZONTAL_OFFSET = - new FloatProperty("adjacentPageHorizontalOffset") { + public static final FloatProperty> ADJACENT_PAGE_HORIZONTAL_OFFSET = + new FloatProperty<>("adjacentPageHorizontalOffset") { @Override public void setValue(RecentsView recentsView, float v) { if (recentsView.mAdjacentPageHorizontalOffset != v) { @@ -307,6 +337,20 @@ public abstract class RecentsView> RUNNING_TASK_ATTACH_ALPHA = + new FloatProperty<>("runningTaskAttachAlpha") { + @Override + public void setValue(RecentsView recentsView, float v) { + recentsView.mRunningTaskAttachAlpha = v; + recentsView.applyAttachAlpha(); + } + + @Override + public Float get(RecentsView recentsView) { + return recentsView.mRunningTaskAttachAlpha; + } + }; + public static final int SCROLL_VIBRATION_PRIMITIVE = Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1; public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f; @@ -317,10 +361,9 @@ public abstract class RecentsView COLOR_TINT = - new FloatProperty("colorTint") { + private static final FloatProperty> COLOR_TINT = + new FloatProperty<>("colorTint") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setColorTint(v); @@ -338,8 +381,8 @@ public abstract class RecentsView TASK_SECONDARY_TRANSLATION = - new FloatProperty("taskSecondaryTranslation") { + public static final FloatProperty> TASK_SECONDARY_TRANSLATION = + new FloatProperty<>("taskSecondaryTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsResistanceTranslation(v); @@ -357,8 +400,8 @@ public abstract class RecentsView TASK_PRIMARY_SPLIT_TRANSLATION = - new FloatProperty("taskPrimarySplitTranslation") { + public static final FloatProperty> TASK_PRIMARY_SPLIT_TRANSLATION = + new FloatProperty<>("taskPrimarySplitTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsPrimarySplitTranslation(v); @@ -370,8 +413,8 @@ public abstract class RecentsView TASK_SECONDARY_SPLIT_TRANSLATION = - new FloatProperty("taskSecondarySplitTranslation") { + public static final FloatProperty> TASK_SECONDARY_SPLIT_TRANSLATION = + new FloatProperty<>("taskSecondarySplitTranslation") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setTaskViewsSecondarySplitTranslation(v); @@ -384,15 +427,12 @@ public abstract class RecentsView RECENTS_SCALE_PROPERTY = - new FloatProperty("recentsScale") { + public static final FloatProperty> RECENTS_SCALE_PROPERTY = + new FloatProperty<>("recentsScale") { @Override public void setValue(RecentsView view, float scale) { view.setScaleX(scale); view.setScaleY(scale); - if (enableRefactorTaskThumbnail()) { - view.mRecentsViewData.getScale().setValue(scale); - } view.mLastComputedTaskStartPushOutDistance = null; view.mLastComputedTaskEndPushOutDistance = null; view.runActionOnRemoteHandles(new Consumer() { @@ -417,8 +457,8 @@ public abstract class RecentsView RECENTS_GRID_PROGRESS = - new FloatProperty("recentsGrid") { + public static final FloatProperty> RECENTS_GRID_PROGRESS = + new FloatProperty<>("recentsGrid") { @Override public void setValue(RecentsView view, float gridProgress) { view.setGridProgress(gridProgress); @@ -430,12 +470,27 @@ public abstract class RecentsView> DESKTOP_CAROUSEL_DETACH_PROGRESS = + new FloatProperty<>("desktopCarouselDetachProgress") { + @Override + public void setValue(RecentsView view, float offset) { + view.mDesktopCarouselDetachProgress = offset; + view.applyAttachAlpha(); + view.updatePageOffsets(); + } + + @Override + public Float get(RecentsView view) { + return view.mDesktopCarouselDetachProgress; + } + }; + /** * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and * being in any other state has a value of 0. */ - public static final FloatProperty TASK_THUMBNAIL_SPLASH_ALPHA = - new FloatProperty("taskThumbnailSplashAlpha") { + public static final FloatProperty> TASK_THUMBNAIL_SPLASH_ALPHA = + new FloatProperty<>("taskThumbnailSplashAlpha") { @Override public void setValue(RecentsView view, float taskThumbnailSplashAlpha) { view.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha); @@ -463,11 +518,8 @@ public abstract class RecentsView mSizeStrategy; + protected final BaseContainerInterface mContainerInterface; @Nullable protected RecentsAnimationController mRecentsAnimationController; @Nullable @@ -483,11 +535,9 @@ public abstract class RecentsView mScrollListeners = new ArrayList<>(); + private final ArraySet mScrollListeners = new ArraySet<>(); // The threshold at which we update the SystemUI flags when animating from the task into the app public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f; @@ -511,19 +561,25 @@ public abstract class RecentsView mGroupedTaskViewPool; private final ViewPool mDesktopTaskViewPool; - private final TaskOverlayFactory mTaskOverlayFactory; + protected final TaskOverlayFactory mTaskOverlayFactory; protected boolean mDisallowScrollToClearAll; + // True if it is not allowed to scroll to [AddDesktopButton]. + protected boolean mDisallowScrollToAddDesk; private boolean mOverlayEnabled; protected boolean mFreezeViewVisibility; private boolean mOverviewGridEnabled; @@ -544,21 +602,22 @@ public abstract class RecentsView mTaskViewsDismissPrimaryTranslations = new HashMap<>(); + /** * TODO: Call reloadIdNeeded in onTaskStackChanged. */ @@ -597,34 +658,39 @@ public abstract class RecentsView( () -> PackageManagerWrapper.getInstance() .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null, MAIN_EXECUTOR, apkRemoved -> { if (apkRemoved) { - dismissTask(taskId); + dismissTask(taskId, /*animate=*/true, /*removeTask=*/false); } else { mModel.isTaskRemoved(taskKey.id, taskRemoved -> { if (taskRemoved) { - dismissTask(taskId); + dismissTask(taskId, /*animate=*/true, /*removeTask=*/false); } - }, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter())); + }, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter(), + mContainer.getDisplayId())); } })); } @@ -637,7 +703,7 @@ public abstract class RecentsView new RecentsRotationStateRepositoryImpl(mOrientationState)); + + recentsDependencies.provide(RecentsDeviceProfileRepository.class, scopeId, + () -> new RecentsDeviceProfileRepositoryImpl(mContainer)); + + recentsDependencies.provide(AppTimersRepository.class, scopeId, + () -> new AppTimersRepositoryImpl( + context.getApplicationContext().getSystemService(LauncherApps.class), + recentsDependencies.inject(DispatcherProvider.class, scopeId) + )); + } else { + mRecentsViewModel = null; + mHelper = null; + } + mScrollHapticMinGapMillis = getResources() .getInteger(R.integer.recentsScrollHapticMinGapMillis); mFastFlingVelocity = getResources() .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); mModel = RecentsModel.INSTANCE.get(context); - mIdp = InvariantDeviceProfile.INSTANCE.get(context); - if (enableRefactorTaskThumbnail()) { - mTasksRepository = new TasksRepository( - mModel, mModel.getThumbnailCache(), mModel.getIconCache()); - } else { - mTasksRepository = null; - } mClearAllButton = (ClearAllButton) LayoutInflater.from(context) .inflate(R.layout.overview_clear_all_button, this, false); mClearAllButton.setOnClickListener(this::dismissAllTasks); + + if (DesktopModeStatus.isMultipleDesktopFrontendEnabledOnDisplay(mContext, + mContainer.getDisplay())) { + mAddDesktopButton = (AddDesktopButton) LayoutInflater.from(context).inflate( + R.layout.overview_add_desktop_button, this, false); + mAddDesktopButton.setOnClickListener(view -> { + AddDesktopButton button = (AddDesktopButton) view; + button.animateVisibility(/* toVisible = */ false, () -> { + createDesk(view); + button.animateVisibility(/* toVisible = */ true); + }); + }); + + mDesktopVisibilityController = DesktopVisibilityController.INSTANCE.get(mContext); + // Update its visibility based on whether we can create a desk or not. + mUtils.onCanCreateDesksChanged( + mDesktopVisibilityController.getCanCreateDesks()); + } + mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */, 10 /* initial size */); + int groupedViewPoolInitialSize = enableRefactorTaskThumbnail() ? 2 : 10; mGroupedTaskViewPool = new ViewPool<>(context, this, - R.layout.task_grouped, 20 /* max size */, 10 /* initial size */); + R.layout.task_grouped, 20 /* max size */, groupedViewPoolInitialSize); + int desktopViewPoolInitialSize = DesktopModeStatus.canEnterDesktopMode(mContext) ? 1 : 0; mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop, - 5 /* max size */, 1 /* initial size */); + 5 /* max size */, desktopViewPoolInitialSize); setOrientationHandler(mOrientationState.getOrientationHandler()); mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources()); @@ -842,15 +978,16 @@ public abstract class RecentsView container.getIconView().getDrawable() != null)) { - tv.onTaskListVisibilityChanged(true /* visible */); + taskView.onTaskListVisibilityChanged(true /* visible */); } } } @@ -1051,42 +1193,36 @@ public abstract class RecentsView thumbnailData, boolean refreshNow) { - TaskView updatedTaskView = null; - for (Map.Entry entry : thumbnailData.entrySet()) { - Integer id = entry.getKey(); - ThumbnailData thumbnail = entry.getValue(); - TaskView taskView = getTaskViewByTaskId(id); - if (taskView == null) { - continue; + /** Updates the thumbnail(s) of the relevant TaskView. */ + public void updateThumbnail(Map thumbnailData) { + if (!enableRefactorTaskThumbnail()) { + for (Map.Entry entry : thumbnailData.entrySet()) { + Integer id = entry.getKey(); + ThumbnailData thumbnail = entry.getValue(); + TaskView taskView = getTaskViewByTaskId(id); + if (taskView == null) { + continue; + } + // taskView could be a GroupedTaskView, so select the relevant task by ID + TaskContainer taskContainer = taskView.getTaskContainerById(id); + if (taskContainer == null) { + continue; + } + Task task = taskContainer.getTask(); + TaskThumbnailViewDeprecated taskThumbnailViewDeprecated = + taskContainer.getThumbnailViewDeprecated(); + taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, /*refreshNow=*/false); } - // taskView could be a GroupedTaskView, so select the relevant task by ID - TaskContainer taskAttributes = taskView.getTaskContainerById(id); - if (taskAttributes == null) { - continue; - } - Task task = taskAttributes.getTask(); - TaskThumbnailViewDeprecated taskThumbnailViewDeprecated = - taskAttributes.getThumbnailViewDeprecated(); - taskThumbnailViewDeprecated.setThumbnail(task, thumbnail, refreshNow); - // thumbnailData can contain 1-2 ids, but they should correspond to the same - // TaskView, so overwriting is ok - updatedTaskView = taskView; } - - return updatedTaskView; } @Override @@ -1097,10 +1233,11 @@ public abstract class RecentsView remoteTargetHandle.getTransformParams() .setSyncTransactionApplier(mSyncTransactionApplier)); - RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this); + RecentsModel.INSTANCE.get(mContext).addThumbnailChangeListener(this); mIPipAnimationListener.setActivityAndRecentsView(mContainer, this); - SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener( + SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener( mIPipAnimationListener); mOrientationState.initListeners(); mTaskOverlayFactory.initListeners(); - if (FeatureFlags.enableSplitContextually()) { - mSplitSelectStateController.registerSplitListener(mSplitSelectionListener); + mSplitSelectStateController.registerSplitListener(mSplitSelectionListener); + if (mDesktopVisibilityController != null) { + mDesktopVisibilityController.registerDesktopVisibilityListener(mUtils); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + updateTaskStackListenerState(); mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this); mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); @@ -1154,51 +1295,89 @@ public abstract class RecentsView remoteTargetHandle.getTransformParams() .setSyncTransactionApplier(null)); executeSideTaskLaunchCallback(); - RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this); - SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null); + RecentsModel.INSTANCE.get(mContext).removeThumbnailChangeListener(this); + SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(null); mIPipAnimationListener.setActivityAndRecentsView(null, null); mOrientationState.destroyListeners(); mTaskOverlayFactory.removeListeners(); - if (FeatureFlags.enableSplitContextually()) { - mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener); + mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener); + if (mDesktopVisibilityController != null) { + mDesktopVisibilityController.unregisterDesktopVisibilityListener(mUtils); } + mTaskLaunchListener = null; + mOnTaskLaunchCancelledRunnable = null; reset(); } + /** + * Execute clean-up logic needed when the view is destroyed. + */ + public void destroy() { + Log.d(TAG, "destroy"); + if (enableRefactorTaskThumbnail()) { + try { + mTaskViewPool.killOngoingInitializations(); + mGroupedTaskViewPool.killOngoingInitializations(); + mDesktopTaskViewPool.killOngoingInitializations(); + } catch (InterruptedException e) { + Log.e(TAG, "Ongoing initializations could not be killed", e); + } + mHelper.onDestroy(); + RecentsDependencies.destroy(getContext()); + } + } + @Override public void onViewRemoved(View child) { + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.onViewRemoved"); super.onViewRemoved(child); - // Clear the task data for the removed child if it was visible unless: // - It's the initial taskview for entering split screen, we only pretend to dismiss the // task // - It's the focused task to be moved to the front, we immediately re-add the task - if (child instanceof TaskView && child != mSplitHiddenTaskView - && child != mMovingTaskView) { - TaskView taskView = (TaskView) child; - for (int i : taskView.getTaskIds()) { - mHasVisibleTaskData.delete(i); + if (child instanceof TaskView) { + mTaskViewCount = Math.max(0, --mTaskViewCount); + if (child != mSplitHiddenTaskView && child != mMovingTaskView) { + clearAndRecycleTaskView((TaskView) child); } - if (child instanceof GroupedTaskView) { - mGroupedTaskViewPool.recycle((GroupedTaskView) taskView); - } else if (child instanceof DesktopTaskView) { - mDesktopTaskViewPool.recycle((DesktopTaskView) taskView); - } else { - mTaskViewPool.recycle(taskView); - } - mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0); } + traceEnd(Trace.TRACE_TAG_APP); + } + + private void clearAndRecycleTaskView(TaskView taskView) { + for (int i : taskView.getTaskIds()) { + mHasVisibleTaskData.delete(i); + } + if (taskView instanceof GroupedTaskView) { + mGroupedTaskViewPool.recycle((GroupedTaskView) taskView); + } else if (taskView instanceof DesktopTaskView) { + mDesktopTaskViewPool.recycle((DesktopTaskView) taskView); + } else { + mTaskViewPool.recycle(taskView); + } + mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, !hasTaskViews()); } @Override public void onViewAdded(View child) { + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.onViewAdded"); super.onViewAdded(child); - child.setAlpha(mContentAlpha); + if (child instanceof TaskView) { + mTaskViewCount++; + } + if (mAddDesktopButton != null && child instanceof AddDesktopButton) { + mAddDesktopButton.setContentAlpha(mContentAlpha); + } else if (child instanceof ClearAllButton) { + mClearAllButton.setContentAlpha(mContentAlpha); + } else { + child.setAlpha(mContentAlpha); + } // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the // child direction back to match system settings. child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL); mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false); updateEmptyMessage(); + traceEnd(Trace.TRACE_TAG_APP); } @Override @@ -1277,12 +1456,13 @@ public abstract class RecentsView= 0; --i) { RemoteAnimationTarget app = apps[i]; - float dx = mContainer.getDeviceProfile().widthPx * (1 - percent) / 2 + float dx = mContainer.getDeviceProfile().getDeviceProperties().getWidthPx() * (1 - percent) / 2 + app.screenSpaceBounds.left * percent; - float dy = mContainer.getDeviceProfile().heightPx * (1 - percent) / 2 + float dy = mContainer.getDeviceProfile().getDeviceProperties().getHeightPx() * (1 - percent) / 2 + app.screenSpaceBounds.top * percent; matrix.setScale(percent, percent); matrix.postTranslate(dx, dy); @@ -1328,13 +1508,13 @@ public abstract class RecentsView= start && taskStart <= end) || (taskEnd >= start - && taskEnd <= end); + + int translatedTaskStart = taskStart + taskViewTranslation; + int translatedTaskEnd = taskEnd + taskViewTranslation; + + taskStart = Math.min(taskStart, translatedTaskStart); + taskEnd = Math.max(taskEnd, translatedTaskEnd); + + return (taskStart >= screenStart && taskStart <= screenEnd) || (taskEnd >= screenStart + && taskEnd <= screenEnd); } private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) { @@ -1410,17 +1617,19 @@ public abstract class RecentsView 0) { @@ -1537,21 +1742,12 @@ public abstract class RecentsView deviceProfile.availableWidthPx * SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - boolean intercept = super.onInterceptTouchEvent(ev); - if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { - Log.d("b/318590728", "onInterceptTouchEvent: " + ev); - } - return intercept; + > deviceProfile.getDeviceProperties().getAvailableWidthPx() * SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE; } @Override @@ -1559,9 +1755,7 @@ public abstract class RecentsView taskGroups) { + protected void applyLoadPlan(List taskGroups, int taskListChangeId) { + if (enableRefactorTaskThumbnail() && !(isAttachedToWindow() + && RecentsDependencies.Companion.hasScope(mContext))) { + // This can happen if a TaskView callback is triggered after the view is destroyed + // (b/404920951). Prevent crashes by returning immediately. + Log.d(TAG, "applyLoadPlan - view invalid, isAttachedToWindow: " + isAttachedToWindow() + + ", hasScope: " + RecentsDependencies.Companion.hasScope(mContext)); + return; + } if (mPendingAnimation != null) { - mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups)); + final List finalTaskGroups = taskGroups; + mPendingAnimation.addEndListener( + success -> applyLoadPlan(finalTaskGroups, taskListChangeId)); return; } @@ -1746,11 +1954,11 @@ public abstract class RecentsView= 0; i--) { GroupTask groupTask = taskGroups.get(i); - boolean isRemovalNeeded = stagedTaskIdToBeRemoved != INVALID_TASK_ID + boolean containsStagedTask = stagedTaskIdToBeRemoved != INVALID_TASK_ID && groupTask.containsTask(stagedTaskIdToBeRemoved); + boolean shouldSkipGroupTask = containsStagedTask && groupTask instanceof SingleTask; - if (isRemovalNeeded && !groupTask.hasMultipleTasks()) { - // If the task we need to remove is not part of a pair, avoiding creating the - // TaskView. + if ((isSplitSelectionActive() && groupTask.taskViewType == TaskViewType.DESKTOP) + || shouldSkipGroupTask) { + // To avoid these tasks from being chosen as the app pair, the creation of a + // TaskView is bypassed. The staged task is already selected for the app pair, + // and the Desktop task should be hidden when selecting a pair. continue; } // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE // to be a temporary container for the remaining task. + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.createTaskView"); TaskView taskView = getTaskViewFromPool( - isRemovalNeeded ? TaskView.Type.SINGLE : groupTask.taskViewType); - if (taskView instanceof GroupedTaskView) { - boolean firstTaskIsLeftTopTask = - groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id; - Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2; - Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1; - ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState, - mTaskOverlayFactory, groupTask.mSplitBounds); - } else if (taskView instanceof DesktopTaskView) { - ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks, - mOrientationState, mTaskOverlayFactory); - mDesktopTaskView = (DesktopTaskView) taskView; + containsStagedTask ? TaskViewType.SINGLE : groupTask.taskViewType); + traceEnd(Trace.TRACE_TAG_APP); + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.bind"); + if (taskView instanceof GroupedTaskView groupedTaskView) { + groupedTaskView.bind((SplitTask) groupTask, mOrientationState, mTaskOverlayFactory); + } else if (taskView instanceof DesktopTaskView desktopTaskView) { + desktopTaskView.bind((DesktopTask) groupTask, mOrientationState, + mTaskOverlayFactory); + } else if (groupTask instanceof SplitTask splitTask) { + Task task = splitTask.getTopLeftTask().key.id == stagedTaskIdToBeRemoved + ? splitTask.getBottomRightTask() + : splitTask.getTopLeftTask(); + taskView.bind(new SingleTask(task), mOrientationState, mTaskOverlayFactory); } else { - Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2 - : groupTask.task1; - taskView.bind(task, mOrientationState, mTaskOverlayFactory); + taskView.bind((SingleTask) groupTask, mOrientationState, mTaskOverlayFactory); } + traceEnd(Trace.TRACE_TAG_APP); + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.forLoop.addTaskView"); addView(taskView); + traceEnd(Trace.TRACE_TAG_APP); // enables instance filtering if the feature flag for it is on if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) { taskView.setUpShowAllInstancesListener(); } } + // For loop end trace + traceEnd(Trace.TRACE_TAG_APP); - if (!taskGroups.isEmpty()) { - addView(mClearAllButton); - } + addView(mClearAllButton); // Keep same previous focused task - TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds); - // If the list changed, maybe the focused task doesn't exist anymore - if (newFocusedTaskView == null && getTaskViewCount() > 0) { - newFocusedTaskView = getTaskViewAt(0); + TaskView newFocusedTaskView = null; + if (!enableGridOnlyOverview()) { + newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds); + if (enableLargeDesktopWindowingTile() + && newFocusedTaskView instanceof DesktopTaskView) { + newFocusedTaskView = null; + } + // If the list changed, maybe the focused task doesn't exist anymore. + if (newFocusedTaskView == null) { + newFocusedTaskView = mUtils.getFirstNonDesktopTaskView(); + } } - mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview() - ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID; - updateTaskSize(); - updateChildTaskOrientations(); + setFocusedTaskViewId( + newFocusedTaskView != null ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID); - TaskView newRunningTaskView = null; - if (hasAllValidTaskIds(runningTaskIds)) { + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.layouts"); + updateTaskSize(); + mUtils.updateChildTaskOrientations(); + traceEnd(Trace.TRACE_TAG_APP); + + TaskView newRunningTaskView = mUtils.getDesktopTaskViewForDeskId(runningTaskViewDeskId); + if (newRunningTaskView == null) { // Update mRunningTaskViewId to be the new TaskView that was assigned by binding // the full list of tasks to taskViews newRunningTaskView = getTaskViewByTaskIds(runningTaskIds); - if (newRunningTaskView != null) { - setRunningTaskViewId(newRunningTaskView.getTaskViewId()); + } + if (newRunningTaskView != null) { + setRunningTaskViewId(newRunningTaskView.getTaskViewId()); + } else { + if (mActiveGestureGroupedTaskInfo != null) { + // This will update mRunningTaskViewId and create a stub view if necessary. + // We try to avoid this because it can cause a scroll jump, but it is needed + // for cases where the running task isn't included in this load plan (e.g. if + // the current running task is excludedFromRecents.) + showCurrentTask(mActiveGestureGroupedTaskInfo, "applyLoadPlan"); + newRunningTaskView = getRunningTaskView(); } else { - if (mActiveGestureRunningTasks != null) { - // This will update mRunningTaskViewId and create a stub view if necessary. - // We try to avoid this because it can cause a scroll jump, but it is needed - // for cases where the running task isn't included in this load plan (e.g. if - // the current running task is excludedFromRecents.) - showCurrentTask(mActiveGestureRunningTasks); - } else { - setRunningTaskViewId(INVALID_TASK_ID); - } + setRunningTaskViewId(INVALID_TASK_ID); } } @@ -1886,37 +2143,25 @@ public abstract class RecentsView 0) { - targetPage = indexOfChild(requireTaskViewAt(0)); - } + targetPage = indexOfChild( + mUtils.getExpectedCurrentTask(newRunningTaskView, newFocusedTaskView)); } if (targetPage != -1 && mCurrentPage != targetPage) { int finalTargetPage = targetPage; - runOnPageScrollsInitialized(() -> { - // TODO(b/246283207): Remove logging once root cause of flake detected. - if (Utilities.isRunningInTestHarness()) { - Log.d("b/246283207", "RecentsView#applyLoadPlan() -> " - + "previousCurrentPage: " + previousCurrentPage - + ", targetPage: " + finalTargetPage - + ", getScrollForPage(targetPage): " - + getScrollForPage(finalTargetPage)); - } - setCurrentPage(finalTargetPage); - }); + runOnPageScrollsInitialized(() -> setCurrentPage(finalTargetPage)); } + traceBegin(Trace.TRACE_TAG_APP, "RecentsView.applyLoadPlan.cleanupStates"); if (mIgnoreResetTaskId != INVALID_TASK_ID && getTaskViewByTaskId(mIgnoreResetTaskId) != ignoreResetTaskView) { // If the taskView mapping is changing, do not preserve the visuals. Since we are @@ -1924,12 +2169,18 @@ public abstract class RecentsView= 0; i--) { - if (i == stubRunningTaskIndex) { - continue; - } - removeView(requireTaskViewAt(i)); - } - if (getTaskViewCount() == 0 && indexOfChild(mClearAllButton) != -1) { + protected void removeAllTaskViews() { + // This handles an edge case where applyLoadPlan happens during a gesture when the only + // Task is one with excludeFromRecents, in which case we should not remove it. + CollectionsKt + .filter(getTaskViews(), taskView -> !isGestureActive() || !taskView.isRunningTask()) + .forEach(this::removeView); + if (!hasTaskViews()) { + removeView(mAddDesktopButton); removeView(mClearAllButton); } } - public int getTaskViewCount() { - int taskViewCount = getChildCount(); - if (indexOfChild(mClearAllButton) != -1) { - taskViewCount--; - } - return taskViewCount; + /** Returns true if there are at least one TaskView has been added to the RecentsView. */ + public boolean hasTaskViews() { + return mUtils.hasTaskViews(); } - public int getGroupedTaskViewCount() { - int groupViewCount = 0; - for (int i = 0; i < getChildCount(); i++) { - if (getChildAt(i) instanceof GroupedTaskView) { - groupViewCount++; - } - } - return groupViewCount; + public int getTaskViewCount() { + return mTaskViewCount; + } + + /** Counts {@link TaskView}s that are not {@link DesktopTaskView} instances. */ + public int getNonDesktopTaskViewCount() { + return mUtils.getNonDesktopTaskViewCount(); } /** @@ -1994,16 +2237,16 @@ public abstract class RecentsView= 0; i--) { - TaskView taskView = requireTaskViewAt(i); + for (TaskView taskView : getTaskViews()) { if (Arrays.stream(taskView.getTaskIds()).noneMatch( taskId -> taskId == mIgnoreResetTaskId)) { taskView.resetViewTransforms(); - taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); + taskView.setIconVisibleForGesture(mTaskIconVisible); taskView.setStableAlpha(mContentAlpha); taskView.setFullscreenProgress(mFullscreenProgress); taskView.setModalness(mTaskModalness); taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha); + taskView.setBorderEnabled(mBorderEnabled); } } // resetTaskVisuals is called at the end of dismiss animation which could update @@ -2016,14 +2259,10 @@ public abstract class RecentsView visibleTaskIds = new ArrayList<>(); - // Update the task data for the in/visible children - for (int i = 0; i < getTaskViewCount(); i++) { - TaskView taskView = requireTaskViewAt(i); + getTaskViews().forEachWithIndexInParent((index, taskView) -> { List containers = taskView.getTaskContainers(); if (containers.isEmpty()) { - continue; + return; } - int index = indexOfChild(taskView); boolean visible; if (showAsGrid()) { - visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd); + visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd, + mTaskViewsDismissPrimaryTranslations.getOrDefault(taskView, 0)); } else { - visible = lower <= index && index <= upper; + visible = index >= lowerIndex && index <= upperIndex; } if (visible) { // Default update all non-null tasks, then remove running ones @@ -2405,18 +2649,12 @@ public abstract class RecentsView task.key.id).collect(Collectors.toList())); - } - if (mTmpRunningTasks != null) { - for (Task t : mTmpRunningTasks) { - // Skip loading if this is the task that we are animating into - // TODO(b/280812109) change this equality check to use A.equals(B) - tasksToUpdate.removeIf(task -> task == t); - } + tasksToUpdate.stream().map((task) -> task.key.id).toList()); } if (tasksToUpdate.isEmpty()) { - continue; + return; } + int visibilityChanges = 0; for (Task task : tasksToUpdate) { if (!mHasVisibleTaskData.get(task.key.id)) { // Ignore thumbnail update if it's current running task during the gesture @@ -2425,25 +2663,32 @@ public abstract class RecentsView - remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false)); - if (!FeatureFlags.enableSplitContextually()) { - resetFromSplitSelectionState(); - } + mBlurUtils.setDrawLiveTileBelowRecents(false); + if (enableCoroutineThreadingImprovements()) { + // TODO(b/391842220): This should not need to be explicitly called from here. When TVs + // are added and removed with the RecentsView lifecycle, this can be removed. + // This is was added because without it cancelling jobs was happening after work was + // scheduled for those jobs resulting in delays. + mUtils.getTaskViews().forEach(TaskView::cancelJobs); + } // These are relatively expensive and don't need to be done this frame (RecentsView isn't // visible anyway), so defer by a frame to get off the critical path, e.g. app to home. - post(() -> { - unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); - setCurrentPage(0); - LayoutUtils.setViewEnabled(mActionsView, true); - if (mOrientationState.setGestureActive(false)) { - updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); - } - }); + // Defer onto the main thread rather than the view message queue since this will not always + // be called in the Recents In Window case. + MAIN_EXECUTOR.getHandler().post(this::onReset); + } + + private void onReset() { + unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); + setCurrentPage(0); + LayoutUtils.setViewEnabled(mActionsView, true); + if (mOrientationState.setGestureActive(false)) { + updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); + } + if (enableRefactorTaskThumbnail()) { + mRecentsViewModel.onReset(); + } } public int getRunningTaskViewId() { @@ -2567,13 +2834,12 @@ public abstract class RecentsView endState = mSizeStrategy.stateFromGestureEndTarget(endTarget); - if (endState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) { - TaskView runningTaskView = getRunningTaskView(); - float runningTaskPrimaryGridTranslation = 0; - float runningTaskSecondaryGridTranslation = 0; - if (runningTaskView != null) { - // Apply the grid translation to running task unless it's being snapped to - // and removes the current translation applied to the running task. - runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX() - - runningTaskView.getNonGridTranslationX(); - runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY(); - } - for (TaskViewSimulator tvs : taskViewSimulators) { - if (animatorSet == null) { - setGridProgress(1); - tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation; - tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation; - } else { - animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1)); - animatorSet.play(tvs.carouselScale.animateToValue(1)); - animatorSet.play(tvs.carouselPrimaryTranslation.animateToValue(0)); - animatorSet.play(tvs.carouselSecondaryTranslation.animateToValue(0)); - animatorSet.play(tvs.taskPrimaryTranslation.animateToValue( - runningTaskPrimaryGridTranslation)); - animatorSet.play(tvs.taskSecondaryTranslation.animateToValue( - runningTaskSecondaryGridTranslation)); - } - } - } - int splashAlpha = endState.showTaskThumbnailSplash() ? 1 : 0; - if (animatorSet == null) { - setTaskThumbnailSplashAlpha(splashAlpha); - } else { - animatorSet.play( - ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, splashAlpha)); - } + AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget, + RemoteTargetHandle[] remoteTargetHandles, boolean isHandlingAtomicEvent) { + mUtils.onPrepareGestureEndAnimation(animatorSet, endTarget, remoteTargetHandles, + isHandlingAtomicEvent); } /** * Called when a gesture from an app has finished, and the animation to the target has ended. */ public void onGestureAnimationEnd() { - mActiveGestureRunningTasks = null; + mActiveGestureGroupedTaskInfo = null; if (mOrientationState.setGestureActive(false)) { updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); } @@ -2790,70 +3014,82 @@ public abstract class RecentsView { + TaskViewSimulator taskViewSimulator = remoteTargetHandle.getTaskViewSimulator(); + // After settling in Overview, recentsScroll will be used to adjust horizontally + // location and taskGridTranslationX doesn't needs to be applied. + taskViewSimulator.taskGridTranslationX.value = 0; + taskViewSimulator.taskGridTranslationY.value = + runningTaskView.getGridTranslationY(); + }); + } + } + mCurrentGestureEndTarget = null; } /** - * Returns true if we should add a stub taskView for the running task id + * Returns true to avoid adding a stub task even when [getRunningTaskViewFromGroupedTaskInfo] + * cannot find a [runningTaskView]. */ - protected boolean shouldAddStubTaskView(Task[] runningTasks) { - int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray(); - TaskView matchingTaskView = null; - if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) { - // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single - // taskId, therefore we match any DesktopTaskView that contains the runningTaskId. - TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]); - if (taskview instanceof DesktopTaskView) { - matchingTaskView = taskview; - } - } else { - matchingTaskView = getTaskViewByTaskIds(runningTaskIds); - } - return matchingTaskView == null; + protected boolean shouldAvoidAddingStubTaskView(GroupedTaskInfo groupedTaskInfo) { + return false; } /** - * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}. + * Creates a task view (if necessary) to represent the tasks with the {@param groupedTaskInfo}. * * All subsequent calls to reload will keep the task as the first item until {@link #reset()} * is called. Also scrolls the view to this task. */ - private void showCurrentTask(Task[] runningTasks) { - Log.d(TAG, "showCurrentTask - runningTasks: " + Arrays.toString(runningTasks)); - if (runningTasks.length == 0) { + private void showCurrentTask(GroupedTaskInfo groupedTaskInfo, String caller) { + Log.d(TAG, "showCurrentTask(" + caller + ") - groupedTaskInfo: " + groupedTaskInfo); + if (groupedTaskInfo == null) { return; } - int runningTaskViewId = -1; - boolean needGroupTaskView = runningTasks.length > 1; - boolean needDesktopTask = hasDesktopTask(runningTasks); - if (shouldAddStubTaskView(runningTasks)) { + + final int runningTaskViewId; + TaskView runningTaskView = mUtils.getRunningTaskViewFromGroupTaskInfo(groupedTaskInfo); + if (runningTaskView != null) { + runningTaskViewId = runningTaskView.getTaskViewId(); + } else if (!shouldAvoidAddingStubTaskView(groupedTaskInfo)) { boolean wasEmpty = getChildCount() == 0; // Add an empty view for now until the task plan is loaded and applied final TaskView taskView; - if (needDesktopTask) { - taskView = getTaskViewFromPool(TaskView.Type.DESKTOP); - mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length); - ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks), - mOrientationState, mTaskOverlayFactory); - } else if (needGroupTaskView) { - taskView = getTaskViewFromPool(TaskView.Type.GROUPED); - mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]}; + if (groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK)) { + taskView = mUtils.createDesktopTaskViewForActiveDesk(groupedTaskInfo); + } else if (groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_SPLIT)) { + taskView = getTaskViewFromPool(TaskViewType.GROUPED); // When we create a placeholder task view mSplitBoundsConfig will be null, but with // the actual app running we won't need to show the thumbnail until all the tasks // load later anyways - ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1], - mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig); + ((GroupedTaskView) taskView).bind( + new SplitTask(Task.from(groupedTaskInfo.getTaskInfo1()), + Task.from(groupedTaskInfo.getTaskInfo2()), mSplitBoundsConfig), + mOrientationState, mTaskOverlayFactory); } else { - taskView = getTaskViewFromPool(TaskView.Type.SINGLE); - // The temporary running task is only used for the duration between the start of the - // gesture and the task list is loaded and applied - mTmpRunningTasks = new Task[]{runningTasks[0]}; - taskView.bind(mTmpRunningTasks[0], mOrientationState, mTaskOverlayFactory); + taskView = getTaskViewFromPool(TaskViewType.SINGLE); + taskView.bind(new SingleTask(Task.from(groupedTaskInfo.getTaskInfo1())), + mOrientationState, mTaskOverlayFactory); } - addView(taskView, 0); + if (mAddDesktopButton != null && wasEmpty) { + addView(mAddDesktopButton); + } + addView(taskView, mUtils.getRunningTaskExpectedIndex(taskView)); runningTaskViewId = taskView.getTaskViewId(); if (wasEmpty) { addView(mClearAllButton); @@ -2864,41 +3100,37 @@ public abstract class RecentsView setCurrentPage(getRunningTaskIndex())); setRunningTaskViewShowScreenshot(false); setRunningTaskHidden(runningTaskTileHidden); // Update task size after setting current task. updateTaskSize(); - updateChildTaskOrientations(); + mUtils.updateChildTaskOrientations(); // Reload the task list reloadIfNeeded(); } - private boolean hasDesktopTask(Task[] runningTasks) { - try { - if (!DesktopModeStatus.canEnterDesktopMode(getContext())) { - return false; - } - } catch (NoClassDefFoundError e) { - // Desktop mode is not supported on this device - return false; - } - for (Task task : runningTasks) { - if (task.key.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) { - return true; - } - } - return false; - } - /** * Sets the running task id, cleaning up the old running task if necessary. */ @@ -2909,7 +3141,7 @@ public abstract class RecentsView updatedThumbnails) { mRunningTaskShowScreenshot = showScreenshot; TaskView runningTaskView = getRunningTaskView(); if (runningTaskView != null) { - runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot); + runningTaskView.setShouldShowScreenshot(mRunningTaskShowScreenshot, updatedThumbnails); + } + if (enableRefactorTaskThumbnail()) { + mRecentsViewModel.setRunningTaskShowScreenshot(showScreenshot); } } - public void setTaskIconScaledDown(boolean isScaledDown) { - if (mTaskIconScaledDown != isScaledDown) { - mTaskIconScaledDown = isScaledDown; - int taskCount = getTaskViewCount(); - for (int i = 0; i < taskCount; i++) { - requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); + /** + * Updates icon visibility when going in or out of overview. + */ + public void setTaskIconVisible(boolean isVisible) { + if (mTaskIconVisible != isVisible) { + mTaskIconVisible = isVisible; + for (TaskView taskView : getTaskViews()) { + taskView.setIconVisibleForGesture(mTaskIconVisible); } } } private void animateActionsViewIn() { if (!showAsGrid() || isFocusedTaskInExpectedScrollPosition()) { - animateActionsViewAlpha(1, TaskView.SCALE_ICON_DURATION); + animateActionsViewAlpha(1, TaskView.FADE_IN_ICON_DURATION); } } - public void animateUpTaskIconScale() { - mTaskIconScaledDown = false; - int taskCount = getTaskViewCount(); - for (int i = 0; i < taskCount; i++) { - TaskView taskView = requireTaskViewAt(i); - taskView.animateIconScaleAndDimIntoView(); + /** + * Updates icon visibility when gesture is settled. + */ + public void startIconFadeInOnGestureComplete() { + mTaskIconVisible = true; + for (TaskView taskView : getTaskViews()) { + taskView.startIconFadeInOnGestureComplete(); } } - /** - * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid - * layout. - * This method is used when no task dismissal has occurred. - */ - private void updateGridProperties() { - updateGridProperties(false, Integer.MAX_VALUE); - } - /** * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid * layout. * - * This method is used when task dismissal has occurred, but rebalance is not needed. - * - * @param isTaskDismissal indicates if update was called due to task dismissal + * Skips rebalance. */ - private void updateGridProperties(boolean isTaskDismissal) { - updateGridProperties(isTaskDismissal, Integer.MAX_VALUE); + protected void updateGridProperties() { + updateGridProperties(null); } /** @@ -3013,83 +3255,112 @@ public abstract class RecentsView gridTranslations = new HashMap<>(); - // Horizontal grid translation for each task - float[] gridTranslations = new float[taskCount]; - - int focusedTaskIndex = Integer.MAX_VALUE; - int focusedTaskShift = 0; - int focusedTaskWidthAndSpacing = 0; + TaskView lastLargeTaskView = mUtils.getLastLargeTaskView(); + int focusedTaskViewShift = 0; + int largeTaskWidthAndSpacing = 0; int snappedTaskRowWidth = 0; + int expectedCurrentTaskRowWidth = 0; int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage(); TaskView snappedTaskView = getTaskViewAt(snappedPage); TaskView homeTaskView = getHomeTaskView(); + // Determine the currentTaskView when going from Home to Overview, and ensure it can be + // snapped to its expected position. + TaskView expectedCurrentTaskView = mUtils.getExpectedCurrentTask(/* runningTaskView= */null, + getFocusedTaskView()); TaskView nextFocusedTaskView = null; - if (!isTaskDismissal) { + // Don't clear the top row, if the user has dismissed a task, to maintain the task order. + if (!mAnyTaskHasBeenDismissed) { mTopRowIdSet.clear(); } - for (int i = 0; i < taskCount; i++) { - TaskView taskView = requireTaskViewAt(i); + + // Consecutive task views in the top row or bottom row, which means another one set will + // be cleared up while starting to add TaskViews to one of them. Also means only one of + // them can be non-empty at most. + Set lastTopTaskViews = new HashSet<>(); + Set lastBottomTaskViews = new HashSet<>(); + + int largeTasksCount = 0; + // True if the last large TaskView has been visited during the TaskViews iteration. + boolean encounteredLastLargeTaskView = false; + // True if the highest index visible TaskView has been visited during the TaskViews + // iteration. + boolean encounteredLastVisibleTaskView = false; + for (TaskView taskView : getTaskViews()) { + if (taskView == lastLargeTaskView) { + encounteredLastLargeTaskView = true; + } + if (taskView == lastVisibleTaskViewDuringDismiss) { + encounteredLastVisibleTaskView = true; + } + float gridTranslation = 0f; int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing; // Evenly distribute tasks between rows unless rearranging due to task dismissal, in // which case keep tasks in their respective rows. For the running task, don't join // the grid. - if (taskView.isFocusedTask()) { - topRowWidth += taskWidthAndSpacing; - bottomRowWidth += taskWidthAndSpacing; - - focusedTaskIndex = i; - focusedTaskWidthAndSpacing = taskWidthAndSpacing; - gridTranslations[i] += focusedTaskShift; - gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; + if (taskView.isLargeTile()) { + largeTasksCount++; + // DesktopTaskView`s are hidden during split select state, so we shouldn't count + // them when calculating row width. + if (!(taskView instanceof DesktopTaskView && isSplitSelectionActive())) { + topRowWidth += taskWidthAndSpacing; + bottomRowWidth += taskWidthAndSpacing; + largeTileRowWidth += taskWidthAndSpacing; + } + gridTranslation += focusedTaskViewShift; + gridTranslation += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; // Center view vertically in case it's from different orientation. taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin - taskView.getLayoutParams().height) / 2f); + largeTaskWidthAndSpacing = taskWidthAndSpacing; + if (taskView == snappedTaskView) { - // If focused task is snapped, the row width is just task width and spacing. - snappedTaskRowWidth = taskWidthAndSpacing; + snappedTaskRowWidth = largeTileRowWidth; + } + if (taskView == expectedCurrentTaskView) { + expectedCurrentTaskRowWidth = largeTileRowWidth; } } else { - if (i > focusedTaskIndex) { - // For tasks after the focused task, shift by focused task's width and spacing. - gridTranslations[i] += - mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; + if (encounteredLastLargeTaskView) { + // For tasks after the last large task, shift by large task's width and spacing. + gridTranslation += + mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing; } else { - // For task before the focused task, accumulate the width and spacing to - // calculate the distance focused task need to shift. - focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; + // For TaskViews before the new focused TaskView, accumulate the width and + // spacing to calculate the distance the new focused TaskView needs to shift. + // This could happen for example after multiple times of dismissing the + // focused TaskView, the triggered rebalance might set a non-first TaskView + // inside `mChildren` as the new focused TaskView. + focusedTaskViewShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; } int taskViewId = taskView.getTaskViewId(); - // Rebalance the grid starting after a certain index boolean isTopRow; - if (isTaskDismissal) { - if (i > startRebalanceAfter) { + if (mAnyTaskHasBeenDismissed) { + // Rebalance the grid starting after a certain index. + if (encounteredLastVisibleTaskView) { mTopRowIdSet.remove(taskViewId); isTopRow = topRowWidth <= bottomRowWidth; } else { @@ -3106,47 +3377,47 @@ public abstract class RecentsView= 0; j--) { - if (j == focusedTaskIndex) { - continue; - } - widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; + for (TaskView bottomTaskView : lastBottomTaskViews) { + widthOffset += bottomTaskView.getLayoutParams().width + mPageSpacing; } float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; - gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX; + gridTranslation += topAccumulatedTranslationX + currentTaskTranslationX; topAccumulatedTranslationX += currentTaskTranslationX; + lastTopTaskViews.add(taskView); + lastBottomTaskViews.clear(); } else { bottomRowWidth += taskWidthAndSpacing; - bottomSet.add(i); // Move into bottom row. taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff); // Move horizontally into empty space. float widthOffset = 0; - for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) { - if (j == focusedTaskIndex) { - continue; - } - widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; + for (TaskView topTaskView : lastTopTaskViews) { + widthOffset += topTaskView.getLayoutParams().width + mPageSpacing; } float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; - gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX; + gridTranslation += bottomAccumulatedTranslationX + currentTaskTranslationX; bottomAccumulatedTranslationX += currentTaskTranslationX; + lastBottomTaskViews.add(taskView); + lastTopTaskViews.clear(); } + int taskViewRowWidth = isTopRow ? topRowWidth : bottomRowWidth; if (taskView == snappedTaskView) { - snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth; + snappedTaskRowWidth = taskViewRowWidth; + } + if (taskView == expectedCurrentTaskView) { + expectedCurrentTaskRowWidth = taskViewRowWidth; } } + gridTranslations.put(taskView, gridTranslation); } // We need to maintain snapped task's page scroll invariant between quick switch and @@ -3157,22 +3428,22 @@ public abstract class RecentsView 0) { // Shift by focused task's width and spacing if a task is focused. clearAllTotalTranslationX += - mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; + mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing; } // Make sure there are enough space between snapped page and ClearAllButton, for the case @@ -3217,28 +3496,34 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() - .taskSecondaryTranslation.value = runningTask.getGridTranslationY() - ); + if (mAddDesktopButton != null) { + TaskView firstTaskView = getFirstTaskView(); + float translationX = 0f; + if (firstTaskView != null) { + translationX += firstTaskView.getGridTranslationX(); + } + if (focusedTaskViewShift != 0) { + // If the focused task is inserted between `firstTaskView` and + // `mAddDesktopButton`, shift `mAddDesktopButton` to accommodate. + translationX += largeTaskWidthAndSpacing; + } + mAddDesktopButton.setGridTranslationX(translationX); } mClearAllButton.setGridTranslationPrimary( @@ -3246,19 +3531,18 @@ public abstract class RecentsView { if (!mEnableDrawingLiveTile) return; - runActionOnRemoteHandles( - remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() - .taskSecondaryTranslation.value = getPagedOrientationHandler() - .getSecondaryValue(taskView.getTranslationX(), - taskView.getTranslationY() - )); + runActionOnRemoteHandles(remoteTargetHandle -> + remoteTargetHandle.getTaskViewSimulator().taskSecondaryTranslation.value = + taskView.getSecondaryDismissTranslationProperty().get(taskView)); redrawLiveTile(); }); } @@ -3378,7 +3625,7 @@ public abstract class RecentsView mSplitSelectStateController.getSplitAnimationController(). playAnimPlaceholderToFullscreen(mContainer, view, - Optional.of(() -> resetFromSplitSelectionState()))); + Optional.of(() -> mSplitSelectStateController.resetState()))); + firstFloatingTaskView.setContentDescription(splitAnimInitProps.getContentDescription()); // SplitInstructionsView: animate in safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView()); @@ -3428,14 +3676,9 @@ public abstract class RecentsView finishRecentsAnimation(true /* toRecents */, - false /* shouldPip */, null /* onFinishComplete */)); - } + switchToScreenshot( + () -> finishRecentsAnimation(true /* toRecents */, + false /* shouldPip */, null /* onFinishComplete */)); } }); anim.addEndListener(success -> { @@ -3443,11 +3686,7 @@ public abstract class RecentsView 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f; + !mTopRowIdSet.isEmpty() && mTopRowIdSet.size() >= (taskCount - 1) / 2f; // Pick the next focused task from the preferred row. - for (int i = 0; i < taskCount; i++) { - TaskView taskView = requireTaskViewAt(i); - if (taskView == dismissedTaskView) { + for (TaskView taskView : getTaskViews()) { + if (taskView == dismissedTaskView || taskView.isLargeTile()) { continue; } boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId()); @@ -3524,288 +3778,178 @@ public abstract class RecentsView v.getVisibility() != GONE && v != dismissedTaskView); - if (count > 1) { - scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); - } } + getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); + getPageScrolls(newScroll, false, + v -> v.getVisibility() != GONE && v != dismissedTaskView); + if (count > 1) { + scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); + } + + isSlidingTasks = isStagingFocusedTask || areAllDesktopTasksDismissed; float dismissTranslationInterpolationEnd = 1; boolean closeGapBetweenClearAll = false; boolean isClearAllHidden = isClearAllHidden(); - boolean snapToLastTask = false; - boolean isLeftRightSplit = - mContainer.getDeviceProfile().isLeftRightSplit && isSplitSelectionActive(); - TaskView lastGridTaskView = showAsGrid ? getLastGridTaskView() : null; - int currentPageScroll = getScrollForPage(mCurrentPage); - int lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView)); - boolean currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll; - if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) { - // After dismissal, animate translation of the remaining tasks to fill any gap left - // between the end of the grid and the clear all button. Only animate if the clear - // all button is visible or would become visible after dismissal. - float longGridRowWidthDiff = 0; - int topGridRowSize = mTopRowIdSet.size(); - int bottomGridRowSize = taskCount - mTopRowIdSet.size() - - (enableGridOnlyOverview() ? 0 : 1); - boolean topRowLonger = topGridRowSize > bottomGridRowSize; - boolean bottomRowLonger = bottomGridRowSize > topGridRowSize; - boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId); - boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed; - if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) { - topGridRowSize--; - } - if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) { - bottomGridRowSize--; - } - int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize) - * (mLastComputedGridTaskSize.width() + mPageSpacing); - if (!enableGridOnlyOverview() && !isStagingFocusedTask) { - longRowWidth += mLastComputedTaskSize.width() + mPageSpacing; - } + if (gridEndData == null) { + gridEndData = mDismissUtils.getGridEndData(dismissedTaskView, + false /* isExpressiveDismiss */, isFocusedTaskDismissed, nextFocusedTaskView, + isStagingFocusedTask, nextFocusedTaskFromTop, nextFocusedTaskWidth); + } + float longGridRowWidthDiff = gridEndData.getGridEndOffset(); + boolean snapToLastTask = gridEndData.getSnapToLastTask(); + float newClearAllShortTotalWidthTranslation = + gridEndData.getNewClearAllShortTotalWidthTranslation(); + boolean currentPageSnapsToEndOfGrid = gridEndData.getCurrentPageSnapsToEndOfGrid(); + if (longGridRowWidthDiff != 0f) { + closeGapBetweenClearAll = true; + } - float gapWidth = 0; - if ((topRowLonger && dismissedTaskFromTop) - || (bottomRowLonger && dismissedTaskFromBottom)) { - gapWidth = dismissedTaskWidth; - } else if (nextFocusedTaskView != null - && ((topRowLonger && nextFocusedTaskFromTop) - || (bottomRowLonger && !nextFocusedTaskFromTop))) { - gapWidth = nextFocusedTaskWidth; - } - if (gapWidth > 0) { - if (mClearAllShortTotalWidthTranslation == 0) { - // Compensate the removed gap if we don't already have shortTotalCompensation, - // and adjust accordingly to the new shortTotalCompensation after dismiss. - int newClearAllShortTotalWidthTranslation = 0; - if (longRowWidth < mLastComputedGridSize.width()) { - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - newClearAllShortTotalWidthTranslation = - (mIsRtl - ? mLastComputedTaskSize.right - : deviceProfile.widthPx - mLastComputedTaskSize.left) - - longRowWidth - deviceProfile.overviewGridSideMargin; - } - float gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation; - longGridRowWidthDiff += mIsRtl ? -gapCompensation : gapCompensation; - } - if (isClearAllHidden) { - // If ClearAllButton isn't fully shown, snap to the last task. - snapToLastTask = true; - } - } - if (isLeftRightSplit && !isStagingFocusedTask) { - // LastTask's scroll is the minimum scroll in split select, if current scroll is - // beyond that, we'll need to snap to last task instead. - TaskView lastTask = getLastGridTaskView(); - if (lastTask != null) { - int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this); - int lastTaskScroll = getScrollForPage(indexOfChild(lastTask)); - if ((mIsRtl && primaryScroll < lastTaskScroll) - || (!mIsRtl && primaryScroll > lastTaskScroll)) { - snapToLastTask = true; - } - } - } - if (snapToLastTask) { - longGridRowWidthDiff += getSnapToLastTaskScrollDiff(); - } else if (isLeftRightSplit && currentPageSnapsToEndOfGrid) { - // Use last task as reference point for scroll diff and snapping calculation as it's - // the only invariant point in landscape split screen. - snapToLastTask = true; - } - - // If we need to animate the grid to compensate the clear all gap, we split the second - // half of the dismiss pending animation (in which the non-dismissed tasks slide into - // place) in half again, making the first quarter the existing non-dismissal sliding - // and the second quarter this new animation of gap filling. This is due to the fact - // that PendingAnimation is a single animation, not a sequence of animations, so we - // fake it using interpolation. - if (longGridRowWidthDiff != 0) { - closeGapBetweenClearAll = true; - // Stagger the offsets of each additional task for a delayed animation. We use - // half here as this animation is half of half of an animation (1/4th). - float halfAdditionalDismissTranslationOffset = - (0.5f * ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET); + // After dismissal, animate translation of the remaining tasks to fill any gap left + // between the end of the grid and the clear all button. Only animate if the clear + // all button is visible or would become visible after dismissal. + if (longGridRowWidthDiff != 0) { + // If we need to animate the grid to compensate the clear all gap, we split the + // second half of the dismiss pending animation (in which the non-dismissed tasks + // slide into place) in half again, making the first quarter the existing + // non-dismissal sliding and the second quarter this new animation of gap filling. + // This is due to the fact that PendingAnimation is a single animation, not a + // sequence of animations, so we fake it using interpolation. Stagger the offsets of + // each additional task for a delayed animation. We use half here as this animation is + // half of half of an animation (1/4th). + float halfAdditionalDismissTranslationOffset = + (0.5f * ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET); + dismissTranslationInterpolationEnd = Utilities.boundToRange( + END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET + + (taskCount - 1) * halfAdditionalDismissTranslationOffset, + END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1); + for (TaskView taskView : getTaskViews()) { + anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, + longGridRowWidthDiff, + clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1)); dismissTranslationInterpolationEnd = Utilities.boundToRange( - END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET - + (taskCount - 1) * halfAdditionalDismissTranslationOffset, + dismissTranslationInterpolationEnd + - halfAdditionalDismissTranslationOffset, END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1); - for (int i = 0; i < taskCount; i++) { - TaskView taskView = requireTaskViewAt(i); - anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff, - clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1)); - dismissTranslationInterpolationEnd = Utilities.boundToRange( - dismissTranslationInterpolationEnd - - halfAdditionalDismissTranslationOffset, - END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1); - if (mEnableDrawingLiveTile && taskView.isRunningTask()) { - anim.addOnFrameCallback(() -> { - runActionOnRemoteHandles( - remoteTargetHandle -> - remoteTargetHandle.getTaskViewSimulator() - .taskPrimaryTranslation.value = - TaskView.GRID_END_TRANSLATION_X.get(taskView)); - redrawLiveTile(); - }); - } - } - - // Change alpha of clear all if translating grid to hide it - if (isClearAllHidden) { - anim.setFloat(mClearAllButton, DISMISS_ALPHA, 0, LINEAR); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mClearAllButton.setDismissAlpha(1); - } + if (mEnableDrawingLiveTile && taskView.isRunningTask()) { + anim.addOnFrameCallback(() -> { + runActionOnRemoteHandles(remoteTargetHandle -> + remoteTargetHandle.getTaskViewSimulator() + .taskPrimaryTranslation.value = + TaskView.GRID_END_TRANSLATION_X.get(taskView)); + redrawLiveTile(); }); } } + + // Change alpha of clear all if translating grid to hide it + if (isClearAllHidden) { + anim.setFloat(mClearAllButton, DISMISS_ALPHA, 0, LINEAR); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mClearAllButton.setDismissAlpha(1); + } + }); + } } SplitAnimationTimings splitTimings = - AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().isTablet); + AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().getDeviceProperties().isTablet()); - int distanceFromDismissedTask = 0; + int distanceFromDismissedTask = 1; + int slidingTranslation = 0; + if (isSlidingTasks) { + int nextSnappedPage = indexOfChild(isStagingFocusedTask + ? mUtils.getFirstSmallTaskView() + : mUtils.getFirstNonDesktopTaskView()); + slidingTranslation = getPagedOrientationHandler().getPrimaryScroll(this) + - getScrollForPage(nextSnappedPage); + slidingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation + : -newClearAllShortTotalWidthTranslation; + } + mTaskViewsDismissPrimaryTranslations.clear(); + int lastTaskViewIndex = indexOfChild(mUtils.getLastTaskView()); for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child == dismissedTaskView) { - if (animateTaskView) { - if (dismissingForSplitSelection) { - createInitialSplitSelectAnimation(anim); - } else { - addDismissedTaskAnimations(dismissedTaskView, duration, anim); - } + if (animateTaskView && !dismissingForSplitSelection) { + addDismissedTaskAnimations(dismissedTaskView, duration, anim); } - } else if (!showAsGrid) { - // Compute scroll offsets from task dismissal for animation. - // If we just take newScroll - oldScroll, everything to the right of dragged task - // translates to the left. We need to offset this in some cases: - // - In RTL, add page offset to all pages, since we want pages to move to the right - // Additionally, add a page offset if: - // - Current page is rightmost page (leftmost for RTL) - // - Dragging an adjacent page on the left side (right side for RTL) - int offset = mIsRtl ? scrollDiffPerPage : 0; - if (mCurrentPage == dismissedIndex) { - int lastPage = taskCount - 1; - if (mCurrentPage == lastPage) { - offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; - } - } else { - // Dismissing an adjacent page. - int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) - if (dismissedIndex == negativeAdjacent) { - offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; - } - } - + } else if (!showAsGrid || (enableLargeDesktopWindowingTile() + && dismissedTaskView != null && dismissedTaskView.isLargeTile() + && nextFocusedTaskView == null && !dismissingForSplitSelection)) { + int offset = getOffsetToDismissedTask(scrollDiffPerPage, dismissedIndex, + lastTaskViewIndex); int scrollDiff = newScroll[i] - oldScroll[i] + offset; if (scrollDiff != 0) { - FloatProperty translationProperty = child instanceof TaskView - ? ((TaskView) child).getPrimaryDismissTranslationProperty() - : getPagedOrientationHandler().getPrimaryViewTranslate(); - - float additionalDismissDuration = - ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs( - i - dismissedIndex); - - // We are in non-grid layout. - // If dismissing for split select, use split timings. - // If not, use dismiss timings. - float animationStartProgress = isSplitSelectionActive() - ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f) - : Utilities.boundToRange( - INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET - + additionalDismissDuration, 0f, 1f); - - float animationEndProgress = isSplitSelectionActive() - ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset() - + splitTimings.getGridSlideDurationOffset(), 0f, 1f) - : 1f; - - // Slide tiles in horizontally to fill dismissed area - anim.setFloat(child, translationProperty, scrollDiff, - clampToProgress( - splitTimings.getGridSlidePrimaryInterpolator(), - animationStartProgress, - animationEndProgress - ) - ); - - if (mEnableDrawingLiveTile && child instanceof TaskView - && ((TaskView) child).isRunningTask()) { - anim.addOnFrameCallback(() -> { - runActionOnRemoteHandles( - remoteTargetHandle -> - remoteTargetHandle.getTaskViewSimulator() - .taskPrimaryTranslation.value = - getPagedOrientationHandler().getPrimaryValue( - child.getTranslationX(), - child.getTranslationY() - )); - redrawLiveTile(); - }); + translateTaskWhenDismissed( + child, + Math.abs(i - dismissedIndex), + scrollDiff, + anim, + splitTimings); + if (child instanceof TaskView taskView) { + mTaskViewsDismissPrimaryTranslations.put(taskView, scrollDiffPerPage); } needsCurveUpdates = true; } - } else if (child instanceof TaskView) { - TaskView taskView = (TaskView) child; - if (isFocusedTaskDismissed) { - if (nextFocusedTaskView != null && - !isSameGridRow(taskView, nextFocusedTaskView)) { - continue; - } - } else { - if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) { - continue; - } - } + } else if (child instanceof TaskView taskView) { // Animate task with index >= dismissed index and in the same row as the // dismissed index or next focused index. Offset successive task dismissal // durations for a staggered effect. - distanceFromDismissedTask++; - int staggerColumn = isStagingFocusedTask + int staggerColumn = isSlidingTasks ? (int) Math.ceil(distanceFromDismissedTask / 2f) : distanceFromDismissedTask; // Set timings based on if user is initiating splitscreen on the focused task, // or splitting/dismissing some other task. - float animationStartProgress = isStagingFocusedTask - ? Utilities.boundToRange( - splitTimings.getGridSlideStartOffset() - + (splitTimings.getGridSlideStaggerOffset() - * staggerColumn), - 0f, - dismissTranslationInterpolationEnd) - : Utilities.boundToRange( - INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET - + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET - * staggerColumn, 0f, dismissTranslationInterpolationEnd); - float animationEndProgress = isStagingFocusedTask - ? Utilities.boundToRange( - splitTimings.getGridSlideStartOffset() - + (splitTimings.getGridSlideStaggerOffset() * staggerColumn) - + splitTimings.getGridSlideDurationOffset(), - 0f, - dismissTranslationInterpolationEnd) - : dismissTranslationInterpolationEnd; - Interpolator dismissInterpolator = isStagingFocusedTask ? OVERSHOOT_0_75 : LINEAR; + final float animationStartProgress; + if (isSlidingTasks) { + float slidingStartOffset = splitTimings.getGridSlideStartOffset() + + (splitTimings.getGridSlideStaggerOffset() * staggerColumn); + if (areAllDesktopTasksDismissed) { + animationStartProgress = Utilities.boundToRange( + slidingStartOffset + + splitTimings.getDesktopFadeSplitAnimationEndOffset(), + 0f, + dismissTranslationInterpolationEnd); + } else { + animationStartProgress = Utilities.boundToRange( + slidingStartOffset, + 0f, + dismissTranslationInterpolationEnd); + } + } else { + animationStartProgress = Utilities.boundToRange( + INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET + + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET + * staggerColumn, 0f, dismissTranslationInterpolationEnd); + } + final float animationEndProgress; + if (isSlidingTasks && taskView != nextFocusedTaskView) { + animationEndProgress = Utilities.boundToRange( + splitTimings.getGridSlideStartOffset() + + (splitTimings.getGridSlideStaggerOffset() * staggerColumn) + + splitTimings.getGridSlideDurationOffset(), + 0f, + dismissTranslationInterpolationEnd); + } else { + animationEndProgress = dismissTranslationInterpolationEnd; + } + + Interpolator dismissInterpolator = isSlidingTasks ? EMPHASIZED : LINEAR; + + float primaryTranslation = 0; if (taskView == nextFocusedTaskView) { // Enlarge the task to be focused next, and translate into focus position. float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width(); anim.setFloat(taskView, TaskView.DISMISS_SCALE, scale, clampToProgress(LINEAR, animationStartProgress, dismissTranslationInterpolationEnd)); - anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), - mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth, - clampToProgress(LINEAR, animationStartProgress, - dismissTranslationInterpolationEnd)); + primaryTranslation += dismissedTaskWidth; float secondaryTranslation = -mTaskGridVerticalDiff; if (!nextFocusedTaskFromTop) { secondaryTranslation -= mTopBottomRowHeightDiff; @@ -3813,27 +3957,42 @@ public abstract class RecentsView= dismissedIndex && isSameGridRow( + taskView, dismissedTaskView))) { + primaryTranslation += nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth; - if (isStagingFocusedTask) { - // Moves less if focused task is not in scroll position. - int focusedTaskScroll = getScrollForPage(dismissedIndex); - int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this); - int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll; - primaryTranslation += - mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff; - } + } + if (!(taskView instanceof DesktopTaskView)) { + primaryTranslation += mIsRtl ? slidingTranslation : -slidingTranslation; + } - anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), - mIsRtl ? primaryTranslation : -primaryTranslation, + if (primaryTranslation != 0) { + float finalTranslation = mIsRtl ? primaryTranslation : -primaryTranslation; + float startTranslation = 0; + if (!(taskView instanceof DesktopTaskView) && slidingTranslation != 0) { + startTranslation = isTaskViewVisible(taskView) ? 0 + : finalTranslation + (mIsRtl ? -mLastComputedTaskSize.right + : mLastComputedTaskSize.right); + } + Animator dismissAnimator = ObjectAnimator.ofFloat(taskView, + taskView.getPrimaryDismissTranslationProperty(), + startTranslation, finalTranslation); + dismissAnimator.setInterpolator( clampToProgress(dismissInterpolator, animationStartProgress, animationEndProgress)); + anim.add(dismissAnimator); + mTaskViewsDismissPrimaryTranslations.put(taskView, (int) finalTranslation); + distanceFromDismissedTask++; } } } + if (dismissingForSplitSelection) { + createInitialSplitSelectAnimation(anim); + } if (needsCurveUpdates) { anim.addOnFrameCallback(this::updateCurveProperties); @@ -3842,21 +4001,26 @@ public abstract class RecentsView InteractionJankMonitorWrapper.begin(this, + Cuj.CUJ_LAUNCHER_OVERVIEW_TASK_DISMISS)); + } mPendingAnimation = anim; final TaskView finalNextFocusedTaskView = nextFocusedTaskView; final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll; final boolean finalSnapToLastTask = snapToLastTask; final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed; - mPendingAnimation.addEndListener(new Consumer() { + mPendingAnimation.addEndListener(new Consumer<>() { @Override public void accept(Boolean success) { - if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) { + if (mEnableDrawingLiveTile && dismissedTaskView != null + && dismissedTaskView.isRunningTask() && success) { finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, - () -> onEnd(success)); + () -> onEnd(true)); } else { onEnd(success); } @@ -3869,17 +4033,21 @@ public abstract class RecentsView removeTaskInternal(dismissedTaskViewId)); + () -> removeGroupTaskInternal(groupTask)); } else { - removeTaskInternal(dismissedTaskViewId); + removeGroupTaskInternal(groupTask); } - announceForAccessibility( - getResources().getString(R.string.task_view_closed)); mContainer.getStatsLogManager().logger() - .withItemInfo(dismissedTaskView.getFirstItemInfo()) + .withItemInfo(dismissedTaskView.getItemInfo()) .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); } @@ -3895,7 +4063,7 @@ public abstract class RecentsView= screenEnd - mPageSpacing; } if (shouldRebalance) { - updateGridProperties(/*isTaskDismissal=*/ true, - highestVisibleTaskIndex); + updateGridProperties(highestVisibleTaskView); updateScrollSynchronously(); } } - IntArray topRowIdArray = getTopRowIdArray(); - IntArray bottomRowIdArray = getBottomRowIdArray(); + IntArray topRowIdArray = mUtils.getTopRowIdArray(); + IntArray bottomRowIdArray = mUtils.getBottomRowIdArray(); if (finalSnapToLastTask) { // If snapping to last task, find the last task after dismissal. pageToSnapTo = indexOfChild( getLastGridTaskView(topRowIdArray, bottomRowIdArray)); + + if (pageToSnapTo == INVALID_PAGE) { + // Snap to latest large tile page after dismissing the + // last grid task. This will prevent snapping to page 0 when + // desktop task is visible as large tile. + pageToSnapTo = indexOfChild(mUtils.getLastLargeTaskView()); + } } else if (taskViewIdToSnapTo != -1) { // If snapping to another page due to indices rearranging, find // the new index after dismissal & rearrange using the task view id. @@ -4044,7 +4222,7 @@ public abstract class RecentsView { + runActionOnRemoteHandles( + remoteTargetHandle -> + remoteTargetHandle.getTaskViewSimulator() + .taskPrimaryTranslation.value = + getPagedOrientationHandler().getPrimaryValue( + view.getTranslationX(), + view.getTranslationY() + )); + redrawLiveTile(); + }); + } + } + /** * Hides all overview actions if user is halfway through split selection, shows otherwise. * We only show split option if: * * Focused view is a single app * * Device is large screen */ - private void updateCurrentTaskActionsVisibility() { + protected void updateCurrentTaskActionsVisibility() { TaskView taskView = getCurrentPageTaskView(); boolean isCurrentSplit = taskView instanceof GroupedTaskView; GroupedTaskView groupedTaskView = isCurrentSplit ? (GroupedTaskView) taskView : null; // Update flags to see if entire actions bar should be hidden. - if (!FeatureFlags.enableAppPairs()) { - mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit); - } mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SELECT_ACTIVE, isSplitSelectionActive()); // Update flags to see if actions bar should show buttons for a single task or a pair of // tasks. - boolean canSaveAppPair = isCurrentSplit && supportsAppPairs() && - getSplitSelectController().getAppPairsController().canSaveAppPair(groupedTaskView); + boolean canSaveAppPair = isCurrentSplit + && !mIs3PLauncher + && getSplitSelectController().getAppPairsController().canSaveAppPair( + groupedTaskView); mActionsView.updateForGroupedTask(isCurrentSplit, canSaveAppPair); boolean isCurrentDesktop = taskView instanceof DesktopTaskView; mActionsView.updateHiddenFlags(HIDDEN_DESKTOP, isCurrentDesktop); } - /** Returns if app pairs are supported in this launcher. Overridden in subclasses. */ - public boolean supportsAppPairs() { - return true; - } - - /** - * Returns all the tasks in the top row, without the focused task - */ - private IntArray getTopRowIdArray() { - if (mTopRowIdSet.isEmpty()) { - return new IntArray(0); - } - IntArray topArray = new IntArray(mTopRowIdSet.size()); - int taskViewCount = getTaskViewCount(); - for (int i = 0; i < taskViewCount; i++) { - int taskViewId = requireTaskViewAt(i).getTaskViewId(); - if (mTopRowIdSet.contains(taskViewId)) { - topArray.add(taskViewId); - } - } - return topArray; - } - - /** - * Returns all the tasks in the bottom row, without the focused task - */ - private IntArray getBottomRowIdArray() { - int bottomRowIdArraySize = getBottomRowTaskCountForTablet(); - if (bottomRowIdArraySize <= 0) { - return new IntArray(0); - } - IntArray bottomArray = new IntArray(bottomRowIdArraySize); - int taskViewCount = getTaskViewCount(); - for (int i = 0; i < taskViewCount; i++) { - int taskViewId = requireTaskViewAt(i).getTaskViewId(); - if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) { - bottomArray.add(taskViewId); - } - } - return bottomArray; - } - /** * Iterate the grid by columns instead of by TaskView index, starting after the focused task and * up to the last balanced column. * - * @return the highest visible TaskView index between both rows + * @return the highest visible TaskView between both rows */ - private int getHighestVisibleTaskIndex() { - if (mTopRowIdSet.isEmpty()) return Integer.MAX_VALUE; // return earlier + protected TaskView getHighestVisibleTaskView() { + if (mTopRowIdSet.isEmpty()) return null; // return earlier - int lastVisibleIndex = Integer.MAX_VALUE; - IntArray topRowIdArray = getTopRowIdArray(); - IntArray bottomRowIdArray = getBottomRowIdArray(); + TaskView lastVisibleTaskView = null; + IntArray topRowIdArray = mUtils.getTopRowIdArray(); + IntArray bottomRowIdArray = mUtils.getBottomRowIdArray(); int balancedColumns = Math.min(bottomRowIdArray.size(), topRowIdArray.size()); for (int i = 0; i < balancedColumns; i++) { @@ -4143,25 +4373,39 @@ public abstract class RecentsView indexOfChild(bottomTask) ? topTask : bottomTask; + } else if (lastVisibleTaskView != null) { break; } } - return lastVisibleIndex; + return lastVisibleTaskView; } - private void removeTaskInternal(int dismissedTaskViewId) { - int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId); - UI_HELPER_EXECUTOR.getHandler().post( - () -> { - for (int taskId : taskIds) { - if (taskId != -1) { - ActivityManagerWrapper.getInstance().removeTask(taskId); - } - } - }); + protected void removeGroupTaskInternal(@NonNull GroupTask groupTask) { + UI_HELPER_EXECUTOR + .getHandler() + .post( + () -> { + if (groupTask instanceof DesktopTask desktopTask) { + if (areMultiDesksFlagsEnabled()) { + SystemUiProxy.INSTANCE + .get(getContext()) + .removeDesk(desktopTask.getDeskId()); + } else if (DesktopModeFlags + .ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { + SystemUiProxy.INSTANCE + .get(getContext()) + .removeDefaultDeskInDisplay( + mContainer.getDisplay().getDisplayId()); + } + } else { + for (Task task : groupTask.getTasks()) { + ActivityManagerWrapper.getInstance().removeTask(task.key.id); + } + } + }); } protected void onDismissAnimationEnds() { @@ -4175,19 +4419,24 @@ public abstract class RecentsView { if (isSuccess) { + // Remove desktops first, since desks can be empty (so they have no recent tasks), + // and closing all tasks on a desk doesn't always necessarily mean that the desk + // will be removed. So, there are no guarantees that the below call to + // `ActivityManagerWrapper::removeAllRecentTasks()` will be enough. + SystemUiProxy.INSTANCE.get(getContext()).removeAllDesks(); + // Remove all the task views now finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> { UI_HELPER_EXECUTOR.getHandler().post( ActivityManagerWrapper.getInstance()::removeAllRecentTasks); - removeTasksViewsAndClearAllButton(); + removeAllTaskViews(); startHome(); }); } @@ -4197,7 +4446,7 @@ public abstract class RecentsView= 0; i--) { - TaskView child = requireTaskViewAt(i); - if (runningTaskView != null && mRunningTaskTileHidden && child == runningTaskView) { - continue; - } - child.setStableAlpha(alpha); + for (TaskView taskView : getTaskViews()) { + taskView.setStableAlpha(alpha); } mClearAllButton.setContentAlpha(mContentAlpha); + + if (mAddDesktopButton != null) { + mAddDesktopButton.setContentAlpha(mContentAlpha); + } int alphaInt = Math.round(alpha * 255); mEmptyMessagePaint.setAlpha(alphaInt); mEmptyIcon.setAlpha(alphaInt); @@ -4359,6 +4662,7 @@ public abstract class RecentsView + remoteTargetHandle.getTaskViewSimulator().setPivotOverride(mTempPointF)); + } } /** @@ -4532,9 +4863,6 @@ public abstract class RecentsView 0; - if (isModalGridWithoutFocusedTask) { - modalMidpoint = indexOfChild(mSelectedTask); + TaskView carouselHiddenMidpointTask = runningTask != null ? runningTask + : mUtils.getFirstTaskViewInCarousel(/*nonRunningTaskCarouselHidden=*/true, + /*runningTaskView=*/null); + int carouselHiddenMidpoint = indexOfChild(carouselHiddenMidpointTask); + boolean shouldCalculateOffsetForAllTasks = showAsGrid + && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile()) + && mTaskModalness > 0; + if (shouldCalculateOffsetForAllTasks) { + modalMidpoint = indexOfChild(getSelectedTaskView()); } float midpointOffsetSize = 0; float leftOffsetSize = midpoint - 1 >= 0 ? getHorizontalOffsetSize(midpoint - 1, midpoint, offset) : 0; - float rightOffsetSize = midpoint + 1 < count - ? getHorizontalOffsetSize(midpoint + 1, midpoint, offset) + int rightOffsetReferenceIndex; + if (areMultiDesksFlagsEnabled() && midpoint == INVALID_PAGE) { + rightOffsetReferenceIndex = getFirstViewIndex(); + } else { + rightOffsetReferenceIndex = midpoint + 1; + } + float rightOffsetSize = rightOffsetReferenceIndex >= 0 && rightOffsetReferenceIndex < count + ? getHorizontalOffsetSize(rightOffsetReferenceIndex, midpoint, offset) : 0; float modalMidpointOffsetSize = 0; float modalLeftOffsetSize = 0; float modalRightOffsetSize = 0; float gridOffsetSize = 0; + float carouselHiddenOffsetSize = 0; if (showAsGrid) { // In grid, we only focus the task on the side. The reference index used for offset @@ -4586,27 +4926,51 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() @@ -4614,11 +4978,11 @@ public abstract class RecentsView translationPropertyY = + taskView.getSecondaryTaskOffsetTranslationProperty(); + translationPropertyY.set(taskView, totalTranslationY); } } updateCurveProperties(); @@ -4635,7 +4999,10 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() @@ -4764,23 +5140,21 @@ public abstract class RecentsView taskContainer.getOverlay().resetModalVisuals()); } } + protected void resetShareUIState() { + mUtils.resetShareUIState(); + } + /** * Primarily used by overview actions to initiate split from focused task, logs the source * of split invocation as such. */ - public void initiateSplitSelect(TaskView taskView) { + public void initiateSplitSelect(TaskContainer taskContainer) { int defaultSplitPosition = getPagedOrientationHandler() .getDefaultSplitPosition(mContainer.getDeviceProfile()); - initiateSplitSelect(taskView, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT); + initiateSplitSelect(taskContainer, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT); } /** TODO(b/266477929): Consolidate this call w/ the one below */ - public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition, + public void initiateSplitSelect(TaskContainer taskContainer, + @StagePosition int stagePosition, StatsLogManager.EventEnum splitEvent) { + TaskView taskView = taskContainer.getTaskView(); mSplitHiddenTaskView = taskView; mSplitSelectStateController.setInitialTaskSelect(null /*intent*/, stagePosition, - taskView.getFirstItemInfo(), splitEvent, taskView.getFirstTask().key.id); + taskContainer.getItemInfo(), splitEvent, taskContainer.getTask().key.id); mSplitSelectStateController.setAnimateCurrentTaskDismissal( true /*animateCurrentTaskDismissal*/); mSplitHiddenTaskViewIndex = indexOfChild(taskView); - updateDesktopTaskVisibility(false /* visible */); } /** @@ -4831,20 +5210,67 @@ public abstract class RecentsView { + if (taskView instanceof DesktopTaskView) { + // Setting pivot to scale down from screen centre. + if (isTaskViewVisible(taskView)) { + float pivotX = 0f; + if (index < mCurrentPage) { + pivotX = mIsRtl ? taskView.getWidth() / 2f - mPageSpacing + - taskView.getWidth() + : taskView.getWidth() / 2f + mPageSpacing + taskView.getWidth(); + } else if (index == mCurrentPage) { + pivotX = taskView.getWidth() / 2f; + } else { + pivotX = mIsRtl ? taskView.getWidth() + mPageSpacing + + taskView.getWidth() + : taskView.getWidth() - mPageSpacing - taskView.getWidth(); + } + taskView.setPivotX(pivotX); + taskView.setPivotY(taskView.getHeight() / 2f); + builder.add(ObjectAnimator + .ofFloat(taskView, TaskView.DISMISS_SCALE, 0.95f), + clampToProgress(timings.getDesktopTaskScaleInterpolator(), 0f, + timings.getDesktopFadeSplitAnimationEndOffset())); + } + builder.addFloat(taskView, SPLIT_ALPHA, 1f, 0f, + clampToProgress(deskTopFadeInterPolator, 0f, + timings.getDesktopFadeSplitAnimationEndOffset())); + } + }); + } + } + + /** + * While exiting from split mode, show all existing DesktopTaskViews. + */ + public void resetDesktopTaskFromSplitSelectState() { + if (enableLargeDesktopWindowingTile()) { + for (TaskView taskView : getTaskViews()) { + if (taskView instanceof DesktopTaskView) { + taskView.setSplitAlpha(1f); + } + } } } @@ -4857,35 +5283,74 @@ public abstract class RecentsView{ - thumbnail.refreshSplashView(); - mSplitHiddenTaskView.updateSnapshotRadius(); + builder.addOnFrameCallback(() -> { + if (!enableRefactorTaskThumbnail()) { + taskContainer.getThumbnailViewDeprecated().refreshSplashView(); + } + mSplitHiddenTaskView.updateFullscreenParams(); }); } else if (isInitiatingSplitFromTaskView) { + mSplitHiddenTaskView.setBorderEnabled(false); // Splitting from Overview for fullscreen task - createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, - true /* dismissingForSplitSelection*/); + if (enableExpressiveDismissTaskMotion() + && (!showAsGrid() || enableGridOnlyOverview())) { + runExpressiveSplit(builder, mSplitHiddenTaskView); + } else { + createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, + true /* dismissingForSplitSelection*/, null /* gridEndData */); + } } else { // Splitting from Home - createInitialSplitSelectAnimation(builder); + TaskView currentPageTaskView = getTaskViewAt(mCurrentPage); + // When current page is a Desktop task it needs special handling to + // display correct animation in split mode + if (currentPageTaskView instanceof DesktopTaskView) { + if (enableExpressiveDismissTaskMotion() + && (!showAsGrid() || enableGridOnlyOverview())) { + runExpressiveSplit(builder, /* taskView= */ null); + } else { + createTaskDismissAnimation(builder, null, true, false, duration, + true /* dismissingForSplitSelection*/, null /* gridEndData */); + } + } else { + createInitialSplitSelectAnimation(builder); + } } } + private void runExpressiveSplit(PendingAnimation builder, @Nullable TaskView taskView) { + createInitialSplitSelectAnimation(builder); + AtomicBoolean hasRunDismiss = new AtomicBoolean(false); + builder.addOnFrameListener((animator) -> { + SplitAnimationTimings splitTimings = + AnimUtils.getDeviceOverviewToSplitTimings( + mContainer.getDeviceProfile().getDeviceProperties().isTablet()); + if (animator.getAnimatedFraction() > splitTimings.getGridSlideStartOffset() + && !hasRunDismiss.get()) { + mDismissUtils.createTaskDismissSpringAnimation( + taskView, false /* shouldRemoveTaskView */, + true /* isSplitSelection */); + hasRunDismiss.set(true); + } + }); + } + /** * Confirms the selection of the next split task. The extra data is passed through because the * user may be selecting a subtask in a group. @@ -4893,17 +5358,21 @@ public abstract class RecentsView { mSplitSelectStateController.launchSplitTasks( aBoolean1 -> { - if (FeatureFlags.enableSplitContextually()) { - mSplitSelectStateController.resetState(); - } else { - resetFromSplitSelectionState(); - } InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER); + mSplitSelectStateController.resetState(); }); }); @@ -4991,25 +5456,22 @@ public abstract class RecentsView, FloatProperty> taskViewsFloat = + Pair>, FloatProperty>> taskViewsFloat = orientationHandler.getSplitSelectTaskOffset( TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION, mContainer.getDeviceProfile()); @@ -5118,15 +5586,8 @@ public abstract class RecentsView 0) { - final View taskView = requireTaskViewAt(0); - requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); - mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), - taskView.getBottom()); - } + mUtils.updateTaskViewDeadZoneRect(mTaskViewDeadZoneRect, mTopRowDeadZoneRect, + mBottomRowDeadZoneRect); } private void updateEmptyStateUi(boolean sizeChanged) { @@ -5139,7 +5600,7 @@ public abstract class RecentsView + remoteTargetHandle.getTaskViewSimulator() + .setPivotOverride(mTempPointF)); + mBlurUtils.setDrawLiveTileBelowRecents(false); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + // If live tile is not launching, reset the pivot applied above. + if (!taskView.isRunningTask()) { + runActionOnRemoteHandles( + remoteTargetHandle -> { + remoteTargetHandle.getTaskViewSimulator().setPivotOverride( + null); + }); + } + } + }); } else if (!showAsGrid) { // We are launching an adjacent task, so parallax the center and other adjacent task. - float displacementX = tv.getWidth() * (toScale - 1f); + float displacementX = taskView.getWidth() * (toScale - 1f); float primaryTranslation = mIsRtl ? -displacementX : displacementX; anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), getPagedOrientationHandler().getPrimaryViewTranslate(), primaryTranslation)); @@ -5225,6 +5719,20 @@ public abstract class RecentsView { + int targetSysUiFlags = taskView.getSysUiStatusNavFlags(); + final boolean[] passedOverviewThreshold = new boolean[]{false}; + AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(taskView); + anim.play(new AnimatedFloat(v -> { // Once we pass a certain threshold, update the sysui flags to match the target // tasks' flags - if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) { + if (v > UPDATE_SYSUI_FLAGS_THRESHOLD) { mContainer.getSystemUiController().updateUiState( UI_STATE_FULLSCREEN_TASK, targetSysUiFlags); } else { @@ -5279,8 +5784,7 @@ public abstract class RecentsView= - SUCCESS_TRANSITION_PROGRESS; + final boolean passed = v >= SUCCESS_TRANSITION_PROGRESS; if (passed != passedOverviewThreshold[0]) { passedOverviewThreshold[0] = passed; performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, @@ -5290,30 +5794,26 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator() - .addOverviewToAppAnim(mPendingAnimation, interpolator)); - mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); + if (taskView.isRunningTask()) { + runActionOnRemoteHandles( + remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() + .addOverviewToAppAnim(mPendingAnimation, interpolator)); + mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); + } + mPendingAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mBlurUtils.setDrawLiveTileBelowRecents(false); + } + }); mPendingAnimation.addEndListener(isSuccess -> { if (isSuccess) { - if (tv instanceof GroupedTaskView && hasAllValidTaskIds(tv.getTaskIds()) + if (taskView instanceof GroupedTaskView && hasAllValidTaskIds(taskView.getTaskIds()) && mRemoteTargetHandles != null) { // TODO(b/194414938): make this part of the animations instead. TaskViewUtils.createSplitAuxiliarySurfacesAnimator( @@ -5323,13 +5823,14 @@ public abstract class RecentsView taskView.launchWithoutAnimation(this::onTaskLaunchAnimationEnd)); } - mContainer.getStatsLogManager().logger().withItemInfo(tv.getFirstItemInfo()) + mContainer.getStatsLogManager().logger().withItemInfo(taskView.getItemInfo()) .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN); } else { onTaskLaunchAnimationEnd(false); @@ -5342,6 +5843,12 @@ public abstract class RecentsView outChildren) { - // Add children in reverse order - for (int i = getChildCount() - 1; i >= 0; --i) { - outChildren.add(getChildAt(i)); - } + outChildren.addAll(mUtils.getAccessibilityChildren()); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + // We only care about TaskView's for the `CollectionInfo` that Talkback uses to read out. final AccessibilityNodeInfo.CollectionInfo - collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( - 1, getTaskViewCount(), false, - AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); + collectionInfo = new AccessibilityNodeInfo.CollectionInfo( + 1, getTaskViewCount(), false); info.setCollectionInfo(collectionInfo); } - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - - final int taskViewCount = getTaskViewCount(); - event.setScrollable(taskViewCount > 0); - - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { - final int[] visibleTasks = getVisibleChildrenRange(); - event.setFromIndex(taskViewCount - visibleTasks[1]); - event.setToIndex(taskViewCount - visibleTasks[0]); - event.setItemCount(taskViewCount); - } - } - @Override public CharSequence getAccessibilityClassName() { // To hear position-in-list related feedback from Talkback. @@ -5407,6 +5904,10 @@ public abstract class RecentsView { TransformParams params = remoteTargetHandle.getTransformParams(); @@ -5416,11 +5917,11 @@ public abstract class RecentsView { final TransformParams params = remoteTargetHandle.getTransformParams(); + if (mContainer instanceof RecentsWindowManager manager) { + params.setHomeBuilderProxy((builder, app, transformParams) -> { + mTmpMatrix.setScale( + 1f, 1f, app.localBounds.exactCenterX(), app.localBounds.exactCenterY()); + builder.setMatrix(mTmpMatrix).setAlpha(1f).setShow(); + }); + } + if (mSyncTransactionApplier != null) { params.setSyncTransactionApplier(mSyncTransactionApplier); params.getTargetSet().addReleaseCheck(mSyncTransactionApplier); @@ -5507,8 +6021,9 @@ public abstract class RecentsView { if (onFinishComplete != null) { onFinishComplete.run(); @@ -5552,6 +6070,9 @@ public abstract class RecentsView 0); + } + return indexOfChild(firstView); } private int getLastViewIndex() { + final View lastView; if (!mDisallowScrollToClearAll) { - return indexOfChild(mClearAllButton); + // When ClearAllButton is present, it always end with ClearAllButton. + lastView = mClearAllButton; + } else if (mShowAsGridLastOnLayout) { + // When ClearAllButton is absent, for the grid Overview, it always end with a grid task + // if they exist, otherwise it ends with a large tile (focused task or desktop task). + TaskView lastGridTaskView = getLastGridTaskView(); + if (lastGridTaskView != null) { + lastView = lastGridTaskView; + } else { + lastView = mUtils.getLastLargeTaskView(); + } + } else { + lastView = mUtils.getLastTaskViewInCarousel( + /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0); } - - if (!mShowAsGridLastOnLayout) { - return getTaskViewCount() - 1; - } - - TaskView lastGridTaskView = getLastGridTaskView(); - if (lastGridTaskView != null) { - return indexOfChild(lastGridTaskView); - } - - // Returns focus task if there are no grid tasks. - return indexOfChild(getFocusedTaskView()); + return indexOfChild(lastView); } /** @@ -5660,42 +6208,55 @@ public abstract class RecentsView { float scrollDiff = taskView.getScrollAdjustment(showAsGrid); - int pageScroll = newPageScrolls[i] + Math.round(scrollDiff); + int pageScroll = newPageScrolls[index] + Math.round(scrollDiff); if ((mIsRtl && pageScroll < lastTaskScroll) || (!mIsRtl && pageScroll > lastTaskScroll)) { pageScroll = lastTaskScroll; } - if (outPageScrolls[i] != pageScroll) { - pageScrollChanged = true; - outPageScrolls[i] = pageScroll; - } + outPageScrolls[index] = pageScroll; if (DEBUG) { - Log.d(TAG, "getPageScrolls - outPageScrolls[" + i + "]: " + outPageScrolls[i]); + Log.d(TAG, + "getPageScrolls - outPageScrolls[" + index + "]: " + outPageScrolls[index]); + } + }); + + int addDesktopButtonIndex = indexOfChild(mAddDesktopButton); + if (addDesktopButtonIndex >= 0 && addDesktopButtonIndex < outPageScrolls.length) { + int firstViewIndex = getFirstViewIndex(); + if (firstViewIndex >= 0 && firstViewIndex < outPageScrolls.length) { + // If we can scroll to [AddDesktopButton], make its page scroll equal to + // the first [TaskView]. Otherwise, make its page scroll out of range of + // [minScroll, maxScroll]. + if (!mDisallowScrollToAddDesk) { + outPageScrolls[addDesktopButtonIndex] = outPageScrolls[firstViewIndex]; + } else { + outPageScrolls[addDesktopButtonIndex] = + outPageScrolls[firstViewIndex] + (mIsRtl ? 1 : -1); + } + } + + if (DEBUG) { + Log.d(TAG, "getPageScrolls - addDesktopButtonScroll: " + + outPageScrolls[addDesktopButtonIndex]); } } if (DEBUG) { Log.d(TAG, "getPageScrolls - clearAllScroll: " + clearAllScroll); } - return pageScrollChanged; + return !Arrays.equals(oldPageScrolls, outPageScrolls); } @Override @@ -5712,12 +6273,12 @@ public abstract class RecentsView 0 - ? (int) Utilities.mapRange( - mAdjacentPageHorizontalOffset, - getOverScrollShift(), - getUndampedOverScrollShift()) - : getOverScrollShift(); + ? (int) Utilities.mapRange( + mAdjacentPageHorizontalOffset, + getOverScrollShift(), + getUndampedOverScrollShift()) + : getOverScrollShift(); return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this) + overScrollShift + getOffsetFromScrollPosition(pageIndex); } @@ -5794,11 +6361,18 @@ public abstract class RecentsView getEventDispatcher(float navbarRotation) { @@ -5882,19 +6468,27 @@ public abstract class RecentsView fullyVisibleTaskIds = new HashSet<>(); + for (TaskView taskView : getTaskViews()) { + if (isTaskViewFullyVisible(taskView)) { + fullyVisibleTaskIds.addAll(taskView.getTaskIdSet()); + } + } + mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds); + } else { + TaskView focusedTaskView = getFocusedTaskView(); + for (TaskView taskView : getTaskViews()) { + if (taskView == focusedTaskView) { + continue; + } + taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView)); + } + // Focus task overlay should be enabled and refreshed at last + if (focusedTaskView != null) { + focusedTaskView.setOverlayEnabled( + mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView)); } - taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView)); - } - // Focus task overlay should be enabled and refreshed at last - if (focusedTaskView != null) { - focusedTaskView.setOverlayEnabled( - mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView)); } } @@ -5902,6 +6496,10 @@ public abstract class RecentsView updatedThumbnails = mUtils.screenshotTasks(taskView); + if (enableRefactorTaskThumbnail()) { + mHelper.switchToScreenshot(taskView, updatedThumbnails, onFinishRunnable); + } else { + setRunningTaskViewShowScreenshot(true, updatedThumbnails); + ViewUtils.postFrameDrawn(taskView, onFinishRunnable); } - ViewUtils.postFrameDrawn(taskView, onFinishRunnable); } /** @@ -5988,9 +6573,12 @@ public abstract class RecentsView getContainerInterface() { + return mContainerInterface; } /** @@ -6055,12 +6650,11 @@ public abstract class RecentsView remoteTargetHandle.getTaskViewSimulator().setScroll(getScrollOffset())); for (int i = mScrollListeners.size() - 1; i >= 0; i--) { - mScrollListeners.get(i).onScrollChanged(); + mScrollListeners.valueAt(i).onScrollChanged(); } } @@ -6290,12 +6892,12 @@ public abstract class RecentsView { if (mRecentsView == null - || mRecentsView.mSizeStrategy.getTaskbarController() == null) { + || mRecentsView.mContainerInterface.getTaskbarController() == null) { return; } // Hide the task bar when leaving PiP to prevent it from flickering once // the app settles in full-screen mode. - mRecentsView.mSizeStrategy.getTaskbarController().onExpandPip(); + mRecentsView.mContainerInterface.getTaskbarController().onExpandPip(); }); } } @@ -6338,7 +6940,7 @@ public abstract class RecentsView finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false, @@ -6350,11 +6952,32 @@ public abstract class RecentsView finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false, + () -> moveTaskToDesktopInternal(taskContainer, successCallback))); + } + + private void moveTaskToDesktopInternal(TaskContainer taskContainer, Runnable successCallback) { + if (mDesktopRecentsTransitionController == null) { + return; + } + mDesktopRecentsTransitionController.moveToExternalDisplay(taskContainer.getTask().key.id); + dismissTaskView(taskContainer.getTaskView(), /*animate*/true, /*removeTask*/false); successCallback.run(); } + // Logs when the orientation of Overview changes. We log both real and fake orientation changes. private void logOrientationChanged() { // Only log when Overview is showing. @@ -6373,7 +6996,42 @@ public abstract class RecentsViewWhen a task dismiss is cancelled, the task will return to its original position via a + * spring animation. As it passes the threshold of its settling state, its neighbors will + * spring in response to the perceived impact of the settling task. + */ + public RecentsDismissUtils.SpringSet runTaskDismissSettlingSpringAnimation( + TaskView draggedTaskView, boolean isDismissing, + RecentsDismissUtils.DismissedTaskData dismissedTaskData, boolean shouldRemoveTaskView, + boolean isSplitSelection) { + return mDismissUtils.createTaskDismissSpringAnimation(draggedTaskView, isDismissing, + dismissedTaskData, shouldRemoveTaskView, isSplitSelection); + } + + /** + * Animates RecentsView's scale to the provided value, using spring animations. + */ + public SpringAnimation animateRecentsScale(float scale) { + return mDismissUtils.animateRecentsScale(scale); + } + public interface TaskLaunchListener { void onTaskLaunched(); } + + /** + * Draws the remote animation targets above the recents view. + * + * @param remoteTargetHandles collection of remoteTargetHandles in Recents. + */ + public void setDrawAboveRecents(RemoteTargetHandle[] remoteTargetHandles) { + mBlurUtils.setDrawAboveRecents(remoteTargetHandles); + } + + public Map getTaskViewsDismissPrimaryTranslations() { + return mTaskViewsDismissPrimaryTranslations; + } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java index 060c71e446..031158d886 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java +++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java @@ -16,7 +16,6 @@ package com.android.quickstep.views; -import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; import android.content.LocusId; @@ -26,11 +25,15 @@ import android.view.MotionEvent; import android.view.View; import android.view.Window; +import androidx.annotation.Nullable; + import com.android.launcher3.BaseActivity; import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.ScrimView; +import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.util.SplitSelectStateController; /** * Interface to be implemented by the parent view of RecentsView @@ -41,7 +44,7 @@ public interface RecentsViewContainer extends ActivityContext { * Returns an instance of an implementation of RecentsViewContainer * @param context will find instance of recentsViewContainer from given context. */ - static T containerFromContext(Context context) { + static T containerFromContext(Context context) { if (context instanceof RecentsViewContainer) { return (T) context; } else if (context instanceof ContextWrapper) { @@ -51,34 +54,21 @@ public interface RecentsViewContainer extends ActivityContext { } } - /** - * Returns {@link SystemUiController} to manage various window flags to control system UI. - */ - SystemUiController getSystemUiController(); - /** * Returns {@link ScrimView} */ ScrimView getScrimView(); + /** + * Returns the BaseContainerInterface to interact with RecentsViewContainer. + */ + > T getContainerInterface(); + /** * Returns the Overview Panel as a View */ T getOverviewPanel(); - /** - * Returns the RootView - */ - View getRootView(); - - /** - * Dispatches a generic motion event to the view hierarchy. - * Returns the current RecentsViewContainer as context - */ - default Context asContext() { - return (Context) this; - } - /** * @see Window.Callback#dispatchGenericMotionEvent(MotionEvent) */ @@ -92,7 +82,7 @@ public interface RecentsViewContainer extends ActivityContext { /** * Returns overview actions view as a view */ - View getActionsView(); + OverviewActionsView getActionsView(); /** * @see BaseActivity#addForceInvisibleFlag(int) @@ -139,12 +129,6 @@ public interface RecentsViewContainer extends ActivityContext { */ void runOnBindToTouchInteractionService(Runnable r); - /** - * @see Activity#getWindow() - * @return Window - */ - Window getWindow(); - /** * @see * BaseActivity#addMultiWindowModeChangedListener(BaseActivity.MultiWindowModeChangedListener) @@ -173,6 +157,25 @@ public interface RecentsViewContainer extends ActivityContext { */ boolean isRecentsViewVisible(); + /** + * Begins transition to start home through container + */ + default void startHome(){ + // no op + } + + /** + * Checks container to see if we can start home transition safely + */ + boolean canStartHomeSafely(); + + + /** + * Enter staged split directly from the current running app. + * @param leftOrTop if the staged split will be positioned left or top. + */ + default void enterStageSplitFromRunningApp(boolean leftOrTop, int displayId) {} + /** * Overwrites any logged item in Launcher that doesn't have a container with the * {@link com.android.launcher3.touch.PagedOrientationHandler} in use for Overview. @@ -198,4 +201,13 @@ public interface RecentsViewContainer extends ActivityContext { .setOrientationHandler(orientationForLogging)) .build()); } + + void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController); + + @Nullable TaskbarUIController getTaskbarUIController(); + + /** + * Returns the Split Select State Controller + */ + SplitSelectStateController getSplitSelectStateController(); } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt new file mode 100644 index 0000000000..d420bc056e --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt @@ -0,0 +1,54 @@ +/* + * 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.quickstep.views + +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.ViewUtils +import com.android.quickstep.recents.viewmodel.RecentsViewModel +import com.android.systemui.shared.recents.model.ThumbnailData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** Helper for [RecentsView] to interact with the [RecentsViewModel]. */ +class RecentsViewModelHelper( + private val recentsViewModel: RecentsViewModel, + private val recentsCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, +) { + fun onDestroy() { + recentsCoroutineScope.cancel("RecentsView is being destroyed") + } + + fun switchToScreenshot( + taskView: TaskView, + updatedThumbnails: Map?, + onFinishRunnable: Runnable, + ) { + // Update recentsViewModel and apply the thumbnailOverride ASAP, before waiting inside + // viewAttachedScope. + recentsViewModel.setRunningTaskShowScreenshot(true) + recentsCoroutineScope.launch(dispatcherProvider.lightweightBackground) { + recentsViewModel.waitForRunningTaskShowScreenshotToUpdate() + recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails) + withContext(dispatcherProvider.main) { + ViewUtils.postFrameDrawn(taskView, onFinishRunnable) + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt new file mode 100644 index 0000000000..abd9787e5b --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt @@ -0,0 +1,778 @@ +/* + * 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.quickstep.views + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.graphics.PointF +import android.graphics.Rect +import android.util.FloatProperty +import android.util.Log +import android.util.Property +import android.view.View +import android.view.View.LAYOUT_DIRECTION_LTR +import android.view.View.LAYOUT_DIRECTION_RTL +import androidx.core.view.children +import androidx.core.view.isInvisible +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU +import com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType +import com.android.launcher3.Flags.enableDesktopExplodedView +import com.android.launcher3.Flags.enableLargeDesktopWindowingTile +import com.android.launcher3.Flags.enableOverviewOnConnectedDisplays +import com.android.launcher3.PagedView.INVALID_PAGE +import com.android.launcher3.R +import com.android.launcher3.Utilities.getPivotsForScalingRectToRect +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID +import com.android.launcher3.statemanager.BaseState +import com.android.launcher3.util.IntArray +import com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu +import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener +import com.android.quickstep.GestureState +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle +import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.isExternalDisplay +import com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS +import com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS +import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA +import com.android.quickstep.views.RecentsView.TAG +import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA +import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.wm.shell.shared.GroupedTaskInfo +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops +import java.util.concurrent.CopyOnWriteArrayList +import java.util.function.BiConsumer +import kotlin.math.min +import kotlin.reflect.KMutableProperty1 + +/** + * Helper class for [RecentsView]. This util class contains refactored and extracted functions from + * RecentsView to facilitate the implementation of unit tests. + */ +class RecentsViewUtils(private val recentsView: RecentsView<*, *>) : DesktopVisibilityListener { + val taskViews = TaskViewsIterable(recentsView) + + /** Callback to be invoked when a new desk is added. */ + interface OnDeskAddedListener { + /** + * Called when a new desk is added. + * + * @param desktopTaskView The [DesktopTaskView] of the new desk. + */ + fun onDeskAdded(desktopTaskView: DesktopTaskView) + } + + private val onDeskAddedListeners = CopyOnWriteArrayList() + + /** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */ + fun screenshotTasks(taskView: TaskView): Map { + val recentsAnimationController = recentsView.recentsAnimationController ?: return emptyMap() + return taskView.taskContainers.associate { + it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id) + } + } + + /** + * Sorts task groups to move desktop tasks to the end of the list. + * + * @param tasks List of group tasks to be sorted. + * @return Sorted list of GroupTasks to be used in the RecentsView. + */ + fun sortDesktopTasksToFront(tasks: List): List { + var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP } + if (areMultiDesksFlagsEnabled()) { + // Desk IDs of newer desks are larger than those of older desks, hence we can use them + // to sort desks from old to new. + desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId } + } + return otherTasks + desktopTasks + } + + fun sortExternalDisplayTasksToFront(tasks: List): List { + val (externalDisplayTasks, otherTasks) = + tasks.partition { it.tasks.firstOrNull().isExternalDisplay } + return otherTasks + externalDisplayTasks + } + + class TaskViewsIterable(val recentsView: RecentsView<*, *>) : Iterable { + /** Iterates TaskViews when its index inside the RecentsView is needed. */ + fun forEachWithIndexInParent(consumer: BiConsumer) { + recentsView.children.forEachIndexed { index, child -> + (child as? TaskView)?.let { consumer.accept(index, it) } + } + } + + override fun iterator(): Iterator = + recentsView.children.mapNotNull { it as? TaskView }.iterator() + } + + /** Counts [TaskView]s that are [DesktopTaskView] instances. */ + private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView } + + /** Counts [TaskView]s that are not [DesktopTaskView] instances. */ + fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView } + + /** Returns a list of all large TaskView Ids from [TaskView]s */ + fun getLargeTaskViewIds(): List = taskViews.filter { it.isLargeTile }.map { it.taskViewId } + + /** Returns a list of all large TaskViews [TaskView]s */ + fun getLargeTaskViews(): List = taskViews.filter { it.isLargeTile } + + /** Returns a list of all non-large TaskViews [TaskView]s */ + fun getSmallTaskViews(): List = taskViews.filter { !it.isLargeTile } + + /** Returns all the TaskViews in the top row, without the focused task */ + fun getTopRowTaskViews(): List = + taskViews.filter { recentsView.mTopRowIdSet.contains(it.taskViewId) } + + /** Returns all the task Ids in the top row, without the focused task */ + fun getTopRowIdArray(): IntArray = + getTopRowTaskViews().map { it.taskViewId }.toLauncher3IntArray() + + /** Returns all the TaskViews in the bottom row, without the focused task */ + fun getBottomRowTaskViews(): List = + taskViews.filter { !recentsView.mTopRowIdSet.contains(it.taskViewId) && !it.isLargeTile } + + /** Returns all the task Ids in the bottom row, without the focused task */ + fun getBottomRowIdArray(): IntArray = + getBottomRowTaskViews().map { it.taskViewId }.toLauncher3IntArray() + + private fun List.toLauncher3IntArray() = + IntArray(size).apply { this@toLauncher3IntArray.forEach(::add) } + + /** Counts [TaskView]s that are large tiles. */ + fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile } + + /** Counts [TaskView]s that are grid tasks. */ + fun getGridTaskCount(): Int = taskViews.count { it.isGridTask } + + /** Returns the first TaskView that should be displayed as a large tile. */ + fun getFirstLargeTaskView(): TaskView? = + taskViews.firstOrNull { + it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView) + } + + /** + * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist. + */ + fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? { + if (deskId == INACTIVE_DESK_ID) { + return null + } + return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId } + as? DesktopTaskView + } + + /** Returns the active desk ID of the display that contains the [recentsView] instance. */ + fun getActiveDeskIdOnThisDisplay(): Int = + DesktopVisibilityController.INSTANCE.get(recentsView.context) + .getActiveDeskId(recentsView.mContainer.display.displayId) + + /** Returns the expected focus task. */ + fun getFirstNonDesktopTaskView(): TaskView? = + if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView } + else taskViews.firstOrNull() + + fun getLastDesktopTaskView(): TaskView? = taskViews.lastOrNull { it is DesktopTaskView } + + /** + * Returns the [TaskView] that should be the current page during task binding, in the following + * priorities: + * 1. Running task + * 2. Focused task + * 3. First non-desktop task + * 4. Last desktop task + * 5. null otherwise + */ + fun getExpectedCurrentTask(runningTaskView: TaskView?, focusedTaskView: TaskView?): TaskView? = + runningTaskView + ?: focusedTaskView + ?: taskViews.firstOrNull { + it !is DesktopTaskView && + (enableOverviewOnConnectedDisplays() || !it.isExternalDisplay) + } + ?: taskViews.lastOrNull() + + private fun getDeviceProfile() = (recentsView.mContainer as RecentsViewContainer).deviceProfile + + fun getRunningTaskExpectedIndex(runningTaskView: TaskView): Int { + if (areMultiDesksFlagsEnabled() && runningTaskView is DesktopTaskView) { + // Use the [deskId] to keep desks in the order of their creation, as a newer desk + // always has a larger [deskId] than the older desks. + val desktopTaskView = + taskViews.firstOrNull { + it is DesktopTaskView && + it.deskId != INACTIVE_DESK_ID && + it.deskId <= runningTaskView.deskId + } + if (desktopTaskView != null) return recentsView.indexOfChild(desktopTaskView) + } + val firstTaskViewIndex = recentsView.indexOfChild(getFirstTaskView()) + return if (getDeviceProfile().deviceProperties.isTablet) { + var index = firstTaskViewIndex + if (enableLargeDesktopWindowingTile() && runningTaskView !is DesktopTaskView) { + // For fullsreen tasks, skip over Desktop tasks in its section + index += + if (runningTaskView.isExternalDisplay) { + taskViews.count { it is DesktopTaskView && it.isExternalDisplay } + } else { + taskViews.count { it is DesktopTaskView && !it.isExternalDisplay } + } + } + if (!runningTaskView.isExternalDisplay) { + // For main display section, skip over external display tasks + index += taskViews.count { it.isExternalDisplay } + } + index + } else { + val currentIndex: Int = recentsView.indexOfChild(runningTaskView) + return if (currentIndex != -1) { + currentIndex // Keep the position if running task already in layout. + } else { + // New running task are added to the front to begin with. + firstTaskViewIndex + } + } + } + + /** Returns the first TaskView if it exists, or null otherwise. */ + fun getFirstTaskView(): TaskView? = taskViews.firstOrNull() + + /** Returns the last TaskView if it exists, or null otherwise. */ + fun getLastTaskView(): TaskView? = taskViews.lastOrNull() + + /** Returns the first TaskView that is not large */ + fun getFirstSmallTaskView(): TaskView? = taskViews.firstOrNull { !it.isLargeTile } + + /** Returns the last TaskView that should be displayed as a large tile. */ + fun getLastLargeTaskView(): TaskView? = taskViews.lastOrNull { it.isLargeTile } + + override fun onCanCreateDesksChanged(canCreateDesks: Boolean) { + recentsView.addDeskButton?.isInvisible = !canCreateDesks + } + + private fun animateDesktopTaskViewSpringIn(desktopTaskView: DesktopTaskView) { + val taskDismissFloatProperty = + FloatPropertyCompat.createFloatPropertyCompat( + desktopTaskView.primaryDismissTranslationProperty + ) + + with(recentsView) { + // Calculate initial translation to bring it offscreen. + val desktopTaskViewIndex = indexOfChild(desktopTaskView) + val midpointIndex = + if (getTaskViewAt(desktopTaskViewIndex + 1) != null) desktopTaskViewIndex + 1 + else INVALID_PAGE + var offscreenTranslationX = + getHorizontalOffsetSize(desktopTaskViewIndex, midpointIndex, 1f) + + // Add 40dp to the offscreen translation. + val additionalOffsetPx = + context.resources.getDimensionPixelSize( + R.dimen.newly_created_desktop_offscreen_position + ) + offscreenTranslationX += if (isRtl) additionalOffsetPx else -additionalOffsetPx + desktopTaskView.primaryDismissTranslationProperty.set( + desktopTaskView, + offscreenTranslationX, + ) + desktopTaskView.isInvisible = false + + val dampingRatio = + context.resources.getFloat(R.dimen.newly_created_desktop_spring_damping_ratio) + val stiffness = + context.resources.getFloat(R.dimen.newly_created_desktop_spring_stiffness) + + SpringAnimation(desktopTaskView, taskDismissFloatProperty) + .setSpring(SpringForce(0f).setDampingRatio(dampingRatio).setStiffness(stiffness)) + .start() + } + } + + override fun onDeskAdded(displayId: Int, deskId: Int) { + with(recentsView) { + // Ignore desk changes that don't belong to this display. + if (displayId != mContainer.displayId) { + return + } + + if (getDesktopTaskViewForDeskId(deskId) != null) { + Log.e(TAG, "A task view for this desk has already been added.") + return + } + + val currentTaskView = currentPageTaskView + + // We assume that a newly added desk is always empty and gets added to the left of the + // `AddNewDesktopButton`. + val desktopTaskView = getTaskViewFromPool(TaskViewType.DESKTOP) as DesktopTaskView + desktopTaskView.bind( + DesktopTask(deskId, displayId, emptyList()), + pagedViewOrientedState, + taskOverlayFactory, + ) + desktopTaskView.isInvisible = true + + val insertionIndex = 1 + indexOfChild(addDeskButton!!) + addView(desktopTaskView, insertionIndex) + updateTaskSize() + updateChildTaskOrientations() + updateScrollSynchronously() + animateDesktopTaskViewSpringIn(desktopTaskView) + + // Set Current Page based on the stored TaskView. + currentTaskView?.let { setCurrentPage(indexOfChild(it)) } + + onDeskAddedListeners.forEach { it.onDeskAdded(desktopTaskView) } + } + } + + override fun onDeskRemoved(displayId: Int, deskId: Int) { + with(recentsView) { + // Ignore desk changes that don't belong to this display. + if (displayId != mContainer.displayId) { + return + } + + // We need to distinguish between desk removals that are triggered from outside of + // overview vs. the ones that were initiated from overview by dismissing the + // corresponding desktop task view. + getDesktopTaskViewForDeskId(deskId)?.let { + dismissTaskView(it, /* animateTaskView= */ true, /* removeTask= */ true) + } + } + } + + /** + * Gets the list of accessibility children. Currently all the children of RecentsViews are + * added, and in the reverse order to the list. + */ + fun getAccessibilityChildren(): List = recentsView.children.toList().reversed() + + @JvmOverloads + /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */ + fun getFirstTaskViewInCarousel( + nonRunningTaskCarouselHidden: Boolean, + runningTaskView: TaskView? = recentsView.runningTaskView, + ): TaskView? = + taskViews.firstOrNull { + it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden) + } + + /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */ + fun getLastTaskViewInCarousel(nonRunningTaskCarouselHidden: Boolean): TaskView? = + taskViews.lastOrNull { + it.isVisibleInCarousel(recentsView.runningTaskView, nonRunningTaskCarouselHidden) + } + + /** Returns if any small tasks are fully visible */ + fun isAnySmallTaskFullyVisible(): Boolean = + taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) } + + /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */ + fun applyAttachAlpha(nonRunningTaskCarouselHidden: Boolean) { + taskViews.forEach { taskView -> + taskView.attachAlpha = + if (taskView == recentsView.runningTaskView) { + RUNNING_TASK_ATTACH_ALPHA.get(recentsView) + } else { + if ( + taskView.isVisibleInCarousel( + recentsView.runningTaskView, + nonRunningTaskCarouselHidden, + ) + ) + 1f + else 0f + } + } + } + + fun TaskView.isVisibleInCarousel( + runningTaskView: TaskView?, + nonRunningTaskCarouselHidden: Boolean, + ): Boolean = + if (!nonRunningTaskCarouselHidden) true + else getCarouselType() == runningTaskView.getCarouselType() + + /** Returns the carousel type of the TaskView, and default to fullscreen if it's null. */ + private fun TaskView?.getCarouselType(): TaskViewCarousel = + if (this is DesktopTaskView) TaskViewCarousel.DESKTOP else TaskViewCarousel.FULL_SCREEN + + private enum class TaskViewCarousel { + FULL_SCREEN, + DESKTOP, + } + + /** Returns true if there are at least one TaskView has been added to the RecentsView. */ + fun hasTaskViews() = taskViews.any() + + fun getTaskContainerById(taskId: Int) = + taskViews.firstNotNullOfOrNull { it.getTaskContainerById(taskId) } + + private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) { + outRowRect.setEmpty() + firstView?.let { + it.getHitRect(TEMP_RECT) + outRowRect.union(TEMP_RECT) + } + lastView?.let { + it.getHitRect(TEMP_RECT) + outRowRect.union(TEMP_RECT) + } + } + + private fun getRowRect(rowTaskViewIds: IntArray, outRowRect: Rect) { + if (rowTaskViewIds.isEmpty) { + outRowRect.setEmpty() + return + } + getRowRect( + recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(0)), + recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(rowTaskViewIds.size() - 1)), + outRowRect, + ) + } + + fun updateTaskViewDeadZoneRect( + outTaskViewRowRect: Rect, + outTopRowRect: Rect, + outBottomRowRect: Rect, + ) { + if (!getDeviceProfile().deviceProperties.isTablet) { + getRowRect(getFirstTaskView(), getLastTaskView(), outTaskViewRowRect) + return + } + getRowRect(getFirstLargeTaskView(), getLastLargeTaskView(), outTaskViewRowRect) + getRowRect(getTopRowIdArray(), outTopRowRect) + getRowRect(getBottomRowIdArray(), outBottomRowRect) + + // Expand large tile Rect to include space between top/bottom row. + val nonEmptyRowRect = + when { + !outTopRowRect.isEmpty -> outTopRowRect + !outBottomRowRect.isEmpty -> outBottomRowRect + else -> return + } + if (recentsView.isRtl) { + if (outTaskViewRowRect.left > nonEmptyRowRect.right) { + outTaskViewRowRect.left = nonEmptyRowRect.right + } + } else { + if (outTaskViewRowRect.right < nonEmptyRowRect.left) { + outTaskViewRowRect.right = nonEmptyRowRect.left + } + } + + // Expand the shorter row Rect to include the space between the 2 rows. + if (outTopRowRect.isEmpty || outBottomRowRect.isEmpty) return + if (outTopRowRect.width() <= outBottomRowRect.width()) { + if (outTopRowRect.bottom < outBottomRowRect.top) { + outTopRowRect.bottom = outBottomRowRect.top + } + } else { + if (outBottomRowRect.top > outTopRowRect.bottom) { + outBottomRowRect.top = outTopRowRect.bottom + } + } + } + + private fun getTaskMenu(): TaskMenuView? = + getTopOpenViewWithType(recentsView.mContainer, TYPE_TASK_MENU) as? TaskMenuView + + fun taskMenuIsOpen(): Boolean { + if (enableOverviewIconMenu()) { + return getTaskMenu()?.isOpen == true + } + return false + } + + fun updateChildTaskOrientations() { + with(recentsView) { + taskViews.forEach { it.setOrientationState(mOrientationState) } + if (enableOverviewIconMenu()) { + children.forEach { + it.layoutDirection = if (isRtl) LAYOUT_DIRECTION_LTR else LAYOUT_DIRECTION_RTL + } + } + + // Return when it's not fake landscape + if (mOrientationState.isRecentsActivityRotationAllowed) return@with + + // Rotation is supported on phone (details at b/254198019#comment4) + getTaskMenu()?.onRotationChanged() + } + } + + fun updateCentralTask() { + val isTablet: Boolean = getDeviceProfile().deviceProperties.isTablet + val actionsViewCanRelateToTaskView = !(isTablet && enableGridOnlyOverview()) + val focusedTaskView = recentsView.focusedTaskView + val currentPageTaskView = recentsView.currentPageTaskView + + fun isInExpectedScrollPosition(taskView: TaskView?) = + taskView?.let { recentsView.isTaskInExpectedScrollPosition(it) } ?: false + + val centralTaskIds: Set = + when { + !actionsViewCanRelateToTaskView -> emptySet() + isTablet && isInExpectedScrollPosition(focusedTaskView) -> + focusedTaskView!!.taskIdSet + isInExpectedScrollPosition(currentPageTaskView) -> currentPageTaskView!!.taskIdSet + else -> emptySet() + } + + recentsView.mRecentsViewModel.updateCentralTaskIds(centralTaskIds) + } + + var deskExplodeProgress: Float = 0f + set(value) { + field = value + taskViews.filterIsInstance().forEach { it.explodeProgress = field } + } + + var selectedTaskView: TaskView? = null + set(newValue) { + val oldValue = field + field = newValue + if (oldValue != newValue) { + onSelectedTaskViewUpdated(oldValue, newValue) + } + } + + private fun onSelectedTaskViewUpdated( + oldSelectedTaskView: TaskView?, + newSelectedTaskView: TaskView?, + ) { + if (!enableGridOnlyOverview()) return + with(recentsView) { + oldSelectedTaskView?.modalScale = 1f + oldSelectedTaskView?.modalPivot = null + + if (newSelectedTaskView == null) return + + val modalTaskBounds = mTempRect + getModalTaskSize(modalTaskBounds) + val selectedTaskBounds = getTaskBounds(newSelectedTaskView) + + // Map bounds to selectedTaskView's coordinate system. + modalTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top) + selectedTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top) + + val modalScale = + min( + (modalTaskBounds.height().toFloat() / selectedTaskBounds.height()), + (modalTaskBounds.width().toFloat() / selectedTaskBounds.width()), + ) + val modalPivot = PointF() + getPivotsForScalingRectToRect(modalTaskBounds, selectedTaskBounds, modalPivot) + + newSelectedTaskView.modalScale = modalScale + newSelectedTaskView.modalPivot = modalPivot + } + } + + /** + * Creates a [DesktopTaskView] for the currently active desk on this display, which contains the + * tasks with the given [groupedTaskInfo]. + */ + fun createDesktopTaskViewForActiveDesk(groupedTaskInfo: GroupedTaskInfo): DesktopTaskView { + val desktopTaskView = + recentsView.getTaskViewFromPool(TaskViewType.DESKTOP) as DesktopTaskView + val tasks: List = groupedTaskInfo.taskInfoList.map { taskInfo -> Task.from(taskInfo) } + desktopTaskView.bind( + DesktopTask(groupedTaskInfo.deskId, groupedTaskInfo.deskDisplayId, tasks), + recentsView.mOrientationState, + recentsView.mTaskOverlayFactory, + ) + return desktopTaskView + } + + fun getRunningTaskViewFromGroupTaskInfo(groupedTaskInfo: GroupedTaskInfo) = + if (enableMultipleDesktops(recentsView.context)) { + if (groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK)) { + getDesktopTaskViewForDeskId(groupedTaskInfo.deskId) + } else { + val runningTaskIds = groupedTaskInfo.taskInfoList.map { it.taskId }.toIntArray() + val taskView = recentsView.getTaskViewByTaskIds(runningTaskIds) + if (taskView?.type == groupedTaskInfo.getTaskViewType()) taskView else null + } + } else { + if ( + groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK) && + groupedTaskInfo.taskInfoList.size == 1 + ) { + recentsView.getTaskViewByTaskId(groupedTaskInfo.taskInfo1!!.taskId) + as? DesktopTaskView + } else { + val runningTaskIds = groupedTaskInfo.taskInfoList.map { it.taskId }.toIntArray() + recentsView.getTaskViewByTaskIds(runningTaskIds) + } + } + + private fun GroupedTaskInfo.getTaskViewType() = + when { + isBaseType(GroupedTaskInfo.TYPE_FULLSCREEN) -> TaskViewType.SINGLE + isBaseType(GroupedTaskInfo.TYPE_SPLIT) -> TaskViewType.GROUPED + isBaseType(GroupedTaskInfo.TYPE_DESK) -> TaskViewType.DESKTOP + else -> null + } + + fun onPrepareGestureEndAnimation( + animatorSet: AnimatorSet, + endTarget: GestureState.GestureEndTarget, + remoteTargetHandles: Array, + isHandlingAtomicEvent: Boolean, + ) { + // Create ObjectAnimator that immediately settles on [endStateValue] when + // [isHandlingAtomicEvent] is true. + fun immediateObjectAnimator( + target: T, + property: Property, + endStateValue: Float, + ) = + if (isHandlingAtomicEvent) + ObjectAnimator.ofFloat(target, property, endStateValue, endStateValue) + else ObjectAnimator.ofFloat(target, property, endStateValue) + + with(recentsView) { + Log.d(TAG, "onPrepareGestureEndAnimation - endTarget: $endTarget") + mCurrentGestureEndTarget = endTarget + val endState: BaseState<*> = mContainerInterface.stateFromGestureEndTarget(endTarget) + + // Starting the desk exploded animation when the gesture from an app is released. + if (enableDesktopExplodedView()) { + animatorSet.play( + ObjectAnimator.ofFloat( + this, + DESK_EXPLODE_PROGRESS, + if (endState.showExplodedDesktopView()) 1f else 0f, + ) + ) + taskViews.filterIsInstance().forEach { + it.remoteTargetHandles = remoteTargetHandles + } + } + + if (endState.displayOverviewTasksAsGrid(getDeviceProfile())) { + updateGridProperties() + animatorSet.play(immediateObjectAnimator(this, RECENTS_GRID_PROGRESS, 1f)) + + val runningTaskView = runningTaskView + var runningTaskGridTranslationX = 0f + var runningTaskGridTranslationY = 0f + if (runningTaskView != null) { + // Apply the grid translation to running task unless it's being snapped to + // and removes the current translation applied to the running task. + runningTaskGridTranslationX = + (runningTaskView.gridTranslationX - runningTaskView.nonGridTranslationX) + runningTaskGridTranslationY = runningTaskView.gridTranslationY + } + remoteTargetHandles.forEach { remoteTargetHandle -> + val taskViewSimulator = remoteTargetHandle.taskViewSimulator + if (enableGridOnlyOverview()) { + animatorSet.play(taskViewSimulator.carouselScale.animateToValue(1f)) + animatorSet.play( + taskViewSimulator.taskGridTranslationX.animateToValue( + runningTaskGridTranslationX + ) + ) + animatorSet.play( + taskViewSimulator.taskGridTranslationY.animateToValue( + runningTaskGridTranslationY + ) + ) + } else { + animatorSet.play( + taskViewSimulator.taskPrimaryTranslation.animateToValue( + runningTaskGridTranslationX + ) + ) + animatorSet.play( + taskViewSimulator.taskSecondaryTranslation.animateToValue( + runningTaskGridTranslationY + ) + ) + } + } + } + animatorSet.play( + immediateObjectAnimator( + this, + TASK_THUMBNAIL_SPLASH_ALPHA, + if (endState.showTaskThumbnailSplash()) 1f else 0f, + ) + ) + if (enableLargeDesktopWindowingTile()) { + animatorSet.play(ObjectAnimator.ofFloat(this, DESKTOP_CAROUSEL_DETACH_PROGRESS, 0f)) + } + + if (enableGridOnlyOverview()) { + // Reload visible tasks according to new [mCurrentGestureEndTarget] value. + loadVisibleTaskData(FLAG_UPDATE_ALL) + } + } + } + + fun resetShareUIState() { + taskViews.flatMap { it.taskContainers }.forEach { it.overlay.resetShareUI() } + } + + /** + * Adds a listener to be notified when a new desk is added. + * + * @param onDeskAddedListener The listener to add. + */ + fun addOnDeskAddedListener(onDeskAddedListener: OnDeskAddedListener) { + onDeskAddedListeners += onDeskAddedListener + } + + /** + * Removes a listener that was previously added to be notified when a new desk is added. + * + * @param onDeskAddedListener The listener to remove. + */ + fun removeOnDeskAddedListener(onDeskAddedListener: OnDeskAddedListener) { + onDeskAddedListeners -= onDeskAddedListener + } + + companion object { + class RecentsViewFloatProperty( + private val utilsProperty: KMutableProperty1 + ) : FloatProperty>(utilsProperty.name) { + override fun get(recentsView: RecentsView<*, *>): Float = + utilsProperty.get(recentsView.mUtils) + + override fun setValue(recentsView: RecentsView<*, *>, value: Float) { + utilsProperty.set(recentsView.mUtils, value) + } + } + + @JvmField + val DESK_EXPLODE_PROGRESS = RecentsViewFloatProperty(RecentsViewUtils::deskExplodeProgress) + + val TEMP_RECT = Rect() + } +} diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java index e86b5a0fd9..fb23039c62 100644 --- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java +++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java @@ -16,12 +16,10 @@ package com.android.quickstep.views; -import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; @@ -40,11 +38,11 @@ import com.android.app.animation.Interpolators; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StateManager; -import com.android.launcher3.states.StateAnimationConfig; +import com.android.quickstep.util.AnimUtils; import com.android.quickstep.util.SplitSelectStateController; +import com.android.wm.shell.shared.TypefaceUtils; +import com.android.wm.shell.shared.TypefaceUtils.FontFamily; /** * A rounded rectangular component containing a single TextView. @@ -56,7 +54,6 @@ import com.android.quickstep.util.SplitSelectStateController; public class SplitInstructionsView extends LinearLayout { private static final int BOUNCE_DURATION = 250; private static final float BOUNCE_HEIGHT = 20; - private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350; private final RecentsViewContainer mContainer; public boolean mIsCurrentlyAnimating = false; @@ -126,36 +123,35 @@ public class SplitInstructionsView extends LinearLayout { } private void init() { - TextView cancelTextView = findViewById(R.id.split_instructions_text); + TextView cancelTextView = findViewById(R.id.split_instructions_text_cancel); TextView instructionTextView = findViewById(R.id.split_instructions_text); - if (FeatureFlags.enableSplitContextually()) { - cancelTextView.setVisibility(VISIBLE); - cancelTextView.setOnClickListener((v) -> exitSplitSelection()); - instructionTextView.setText(R.string.toast_contextual_split_select_app); + cancelTextView.setVisibility(VISIBLE); + cancelTextView.setOnClickListener((v) -> exitSplitSelection()); + instructionTextView.setText(R.string.toast_contextual_split_select_app); + TypefaceUtils.setTypeface(instructionTextView, FontFamily.GSF_BODY_MEDIUM); - // After layout, expand touch target of cancel button to meet minimum a11y measurements. - post(() -> { - int minTouchSize = getResources() - .getDimensionPixelSize(R.dimen.settingslib_preferred_minimum_touch_target); - Rect r = new Rect(); - cancelTextView.getHitRect(r); + // After layout, expand touch target of cancel button to meet minimum a11y measurements. + post(() -> { + int minTouchSize = getResources() + .getDimensionPixelSize(R.dimen.settingslib_preferred_minimum_touch_target); + Rect r = new Rect(); + cancelTextView.getHitRect(r); - if (r.width() < minTouchSize) { - // add 1 to ensure ceiling on int division - int expandAmount = (minTouchSize + 1 - r.width()) / 2; - r.left -= expandAmount; - r.right += expandAmount; - } - if (r.height() < minTouchSize) { - int expandAmount = (minTouchSize + 1 - r.height()) / 2; - r.top -= expandAmount; - r.bottom += expandAmount; - } + if (r.width() < minTouchSize) { + // add 1 to ensure ceiling on int division + int expandAmount = (minTouchSize + 1 - r.width()) / 2; + r.left -= expandAmount; + r.right += expandAmount; + } + if (r.height() < minTouchSize) { + int expandAmount = (minTouchSize + 1 - r.height()) / 2; + r.top -= expandAmount; + r.bottom += expandAmount; + } - setTouchDelegate(new TouchDelegate(r, cancelTextView)); - }); - } + setTouchDelegate(new TouchDelegate(r, cancelTextView)); + }); // Set accessibility title, will be announced by a11y tools. if (Utilities.ATLEAST_P) { @@ -166,25 +162,11 @@ public class SplitInstructionsView extends LinearLayout { private void exitSplitSelection() { RecentsView recentsView = mContainer.getOverviewPanel(); SplitSelectStateController splitSelectController = recentsView.getSplitSelectController(); - StateManager stateManager = recentsView.getStateManager(); - BaseState startState = stateManager.getState(); - long duration = startState.getTransitionDuration(mContainer.asContext(), false); - if (duration == 0) { - // Case where we're in contextual on workspace (NORMAL), which by default has 0 - // transition duration - duration = DURATION_DEFAULT_SPLIT_DISMISS; - } - StateAnimationConfig config = new StateAnimationConfig(); - config.duration = duration; - AnimatorSet stateAnim = stateManager.createAtomicAnimation( - startState, NORMAL, config); - AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController() - .createPlaceholderDismissAnim(mContainer, - LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration); - stateAnim.play(dismissAnim); - stateManager.setCurrentAnimation(stateAnim, NORMAL); - stateAnim.start(); + + AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer, + LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, + splitSelectController.getSplitAnimationController()); } void ensureProperRotation() { diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt new file mode 100644 index 0000000000..83458324f2 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt @@ -0,0 +1,246 @@ +/* + * 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.quickstep.views + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.view.View +import android.view.View.OnClickListener +import com.android.app.tracing.traceSection +import com.android.launcher3.Flags.enableRefactorTaskContentView +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.model.data.TaskViewItemInfo +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.ViewUtils.addAccessibleChildToList +import com.android.quickstep.recents.domain.usecase.ThumbnailPosition +import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.thumbnail.TaskContentView +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData + +/** Holder for all Task dependent information. */ +class TaskContainer( + val taskView: TaskView, + val task: Task, + // TODO(b/409248525): Upon flag cleanup, use the `TaskContentView` type + val taskContentView: View, + val snapshotView: View, + val iconView: TaskViewIcon, + /** + * This technically can be a vanilla [android.view.TouchDelegate] class, however that class + * requires setting the touch bounds at construction, so we'd repeatedly be created many + * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows touch + * delegated bounds only to be updated. + */ + val iconTouchDelegate: TransformingTouchDelegate, + /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */ + @SplitConfigurationOptions.StagePosition val stagePosition: Int, + val digitalWellBeingToast: DigitalWellBeingToast?, + val showWindowsView: View?, + taskOverlayFactory: TaskOverlayFactory, +) { + val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this) + var thumbnailPosition: ThumbnailPosition? = null + private var overlayEnabledStatus = false + + init { + when { + enableRefactorTaskContentView() -> { + require(taskContentView is TaskContentView) + require(snapshotView is TaskThumbnailView) + } + enableRefactorTaskThumbnail() -> { + require(taskContentView is TaskThumbnailView) + require(snapshotView is TaskThumbnailView) + } + else -> { + require(taskContentView is TaskThumbnailViewDeprecated) + require(snapshotView is TaskThumbnailViewDeprecated) + } + } + } + + internal var thumbnailData: ThumbnailData? = null + private set + + val thumbnail: Bitmap? + /** If possible don't use this. It should be replaced as part of b/331753115. */ + get() = + if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail + else thumbnailViewDeprecated.thumbnail + + val thumbnailView: TaskThumbnailView + get() { + require(enableRefactorTaskThumbnail()) + return snapshotView as TaskThumbnailView + } + + val thumbnailViewDeprecated: TaskThumbnailViewDeprecated + get() { + require(!enableRefactorTaskThumbnail()) + return snapshotView as TaskThumbnailViewDeprecated + } + + var isThumbnailValid: Boolean = false + internal set + + val shouldShowSplashView: Boolean + get() = + if (enableRefactorTaskThumbnail()) taskView.shouldShowSplash() + else thumbnailViewDeprecated.shouldShowSplashView() + + /** Builds proto for logging */ + val itemInfo: TaskViewItemInfo + get() = TaskViewItemInfo(taskView, this) + + fun bind() = { + digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition) + if (!enableRefactorTaskThumbnail()) { + thumbnailViewDeprecated.bind(task, overlay, taskView) + } + } + + fun destroy() = { + digitalWellBeingToast?.destroy() + taskContentView.scaleX = 1f + taskContentView.scaleY = 1f + overlay.reset() + if (enableRefactorTaskThumbnail()) { + isThumbnailValid = false + thumbnailData = null + thumbnailView.onRecycle() + } else { + thumbnailViewDeprecated.setShowSplashForSplitSelection(false) + } + + if (enableOverviewIconMenu() && taskView.type != TaskViewType.DESKTOP) { + (iconView as IconAppChipView).reset() + } + } + + fun setOverlayEnabled(enabled: Boolean) { + if (!enableRefactorTaskThumbnail()) { + thumbnailViewDeprecated.setOverlayEnabled(enabled) + } + } + + fun setOverlayEnabled(enabled: Boolean, thumbnailPosition: ThumbnailPosition) { + if (enableRefactorTaskThumbnail()) { + if (overlayEnabledStatus != enabled || this.thumbnailPosition != thumbnailPosition) { + overlayEnabledStatus = enabled + + refreshOverlay(thumbnailPosition) + } + } + } + + fun refreshOverlay(thumbnailPosition: ThumbnailPosition) = { + this.thumbnailPosition = thumbnailPosition + if (overlayEnabledStatus) { + overlay.initOverlay( + task, + thumbnailData?.thumbnail, + thumbnailPosition.matrix, + thumbnailPosition.isRotated, + ) + } else { + overlay.reset() + } + } + + fun addChildForAccessibility(outChildren: ArrayList) { + addAccessibleChildToList(iconView.asView(), outChildren) + addAccessibleChildToList( + if (enableRefactorTaskContentView()) taskContentView else snapshotView, + outChildren, + ) + showWindowsView?.let { addAccessibleChildToList(it, outChildren) } + digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) } + overlay.addChildForAccessibility(outChildren) + } + + fun setState( + state: TaskData?, + hasHeader: Boolean, + canShowAppTimer: Boolean, + clickCloseListener: OnClickListener?, + ) = { + if (enableRefactorTaskContentView()) { + (taskContentView as TaskContentView).setState( + TaskUiStateMapper.toTaskHeaderState(state, hasHeader, clickCloseListener), + TaskUiStateMapper.toTaskThumbnailUiState(state), + TaskUiStateMapper.toTaskAppTimerUiState(canShowAppTimer, stagePosition, state), + state?.taskId, + ) + } else { + thumbnailView.setState( + TaskUiStateMapper.toTaskThumbnailUiState(state), + state?.taskId, + ) + } + thumbnailData = if (state is TaskData.Data) state.thumbnailData else null + overlay.setThumbnailState(thumbnailData) + } + + fun updateTintAmount(tintAmount: Float) { + thumbnailView.updateTintAmount(tintAmount) + } + + /** + * Updates the progress of the menu opening animation. + * + * This function propagates the given `progress` value to the `thumbnailView` allowing the + * thumbnail view to animate its visual state in sync with the menu's opening/closing + * transition. + * + * @param progress The progress of the menu opening animation (from closed=0 to fully open=1) + */ + fun updateMenuOpenProgress(progress: Float) { + thumbnailView.updateMenuOpenProgress(progress) + } + + /** + * Updates the thumbnail splash progress for a given task. + * + * This function manages the visual feedback of a "splash" effect that can be displayed over a + * thumbnail image, typically during loading or updating. It calculates the alpha (transparency) + * of the splash based on the provided progress and then applies this alpha to the thumbnail + * view if it should be displayed. + * + * @param progress The progress of the operation, ranging from 0.0 to 1.0 + */ + fun updateThumbnailSplashProgress(progress: Float) { + if (enableRefactorTaskThumbnail()) { + thumbnailView.updateSplashAlpha(progress) + } else { + thumbnailViewDeprecated.setSplashAlpha(progress) + } + } + + fun updateThumbnailMatrix(matrix: Matrix) { + thumbnailView.setImageMatrix(matrix) + } + + companion object { + const val TAG = "TaskContainer" + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskHeaderView.kt b/quickstep/src/com/android/quickstep/views/TaskHeaderView.kt new file mode 100644 index 0000000000..bb0044161d --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskHeaderView.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.TouchDelegate +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isGone +import com.android.launcher3.R +import com.android.quickstep.task.thumbnail.TaskHeaderUiState + +class TaskHeaderView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs) { + + private val headerTitleView: TextView by lazy { findViewById(R.id.header_app_title) } + private val headerIconView: ImageView by lazy { findViewById(R.id.header_app_icon) } + private val headerCloseButton: ImageButton by lazy { findViewById(R.id.header_close_button) } + + override fun onFinishInflate() { + super.onFinishInflate() + // Post to ensure the button has been laid out and has its dimensions. + headerCloseButton.post { + val delegateArea = Rect() + headerCloseButton.getHitRect(delegateArea) + + // Calculate the desired touch area size in pixels. + val touchTargetSize = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_header_close_button_hit_rect_size + ) + + // Expand the hit rect to the desired touch target size, centered on the button. + val sizeDifferenceX = (touchTargetSize - delegateArea.width()) / 2 + val sizeDifferenceY = (touchTargetSize - delegateArea.height()) / 2 + delegateArea.inset(-sizeDifferenceX, -sizeDifferenceY) + + // The TouchDelegate is set on the parent `TaskHeaderView`. + touchDelegate = TouchDelegate(delegateArea, headerCloseButton) + } + } + + fun setState(taskHeaderState: TaskHeaderUiState) { + when (taskHeaderState) { + is TaskHeaderUiState.ShowHeader -> { + setHeader(taskHeaderState.header) + isGone = false + } + TaskHeaderUiState.HideHeader -> isGone = true + } + } + + private fun setHeader(header: TaskHeaderUiState.ThumbnailHeader) { + headerTitleView.text = header.title + headerIconView.setImageDrawable(header.icon) + headerCloseButton.setOnClickListener(header.clickCloseListener) + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.kt b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt new file mode 100644 index 0000000000..24924c13a4 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.TimeInterpolator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RectShape +import android.util.AttributeSet +import android.view.Gravity +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.android.app.animation.Interpolators +import com.android.app.animation.Interpolators.clampToProgress +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.R +import com.android.launcher3.anim.AnimationSuccessListener +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.views.BaseDragLayer +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.TaskUtils +import com.android.quickstep.orientation.RecentsPagedOrientationHandler +import com.android.quickstep.util.TaskCornerRadius +import java.util.function.Consumer +import kotlin.math.max +import kotlin.math.roundToInt + +/** Contains options for a recent task when long-pressing its icon. */ +class TaskMenuView +@JvmOverloads +constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int = 0) : + AbstractFloatingView(context, attrs, defStyleAttr) { + private val recentsViewContainer: RecentsViewContainer = + RecentsViewContainer.containerFromContext(context) + private val tempRect = Rect() + private val taskName: TextView by lazy { findViewById(R.id.task_name) } + private val optionLayout: LinearLayout by lazy { findViewById(R.id.menu_option_layout) } + private var openCloseAnimator: AnimatorSet? = null + private var revealAnimator: ValueAnimator? = null + private var onClosingStartCallback: Runnable? = null + private lateinit var orientationHandler: RecentsPagedOrientationHandler + private lateinit var taskView: TaskView + private lateinit var taskContainer: TaskContainer + private var menuTranslationXBeforeOpen = 0f + private var menuTranslationYBeforeOpen = 0f + + // Spaced claimed below Overview (taskbar and insets) + private val taskbarTop by lazy { + recentsViewContainer.deviceProfile.deviceProperties.heightPx - + recentsViewContainer.deviceProfile.overviewActionsClaimedSpaceBelow + } + private val minMenuTop by lazy { taskContainer.iconView.asView().height.toFloat() } + private val maxMenuBottom by lazy { taskbarTop - recentsViewContainer.dragLayer.insets.top } + + init { + clipToOutline = true + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + if (ev.action == MotionEvent.ACTION_DOWN) { + if (!recentsViewContainer.dragLayer.isEventOverView(this, ev)) { + // TODO: log this once we have a new container type for it? + animateOpenOrClosed(true) + return true + } + } + return false + } + + override fun handleClose(animate: Boolean) { + animateOpenOrClosed(closing = true, animated = false) + } + + override fun isOfType(type: Int): Boolean = (type and TYPE_TASK_MENU) != 0 + + override fun getOutlineProvider(): ViewOutlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0, + 0, + view.width, + view.height, + TaskCornerRadius.get(view.context), + ) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + var heightMeasure = heightMeasureSpec + val maxMenuHeight = calculateMaxHeight() + if (MeasureSpec.getSize(heightMeasure) > maxMenuHeight) { + heightMeasure = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST) + } + super.onMeasure(widthMeasureSpec, heightMeasure) + } + + fun onRotationChanged() { + openCloseAnimator?.let { if (it.isRunning) it.end() } + if (mIsOpen) { + optionLayout.removeAllViews() + if (enableOverviewIconMenu() || !populateAndLayoutMenu()) { + close(false) + } + } + } + + private fun populateAndShowForTask(taskContainer: TaskContainer): Boolean { + if (isAttachedToWindow) return false + recentsViewContainer.dragLayer.addView(this) + taskView = taskContainer.taskView + this.taskContainer = taskContainer + if (!populateAndLayoutMenu()) return false + post { this.animateOpen() } + return true + } + + /** @return true if successfully able to populate task view menu, false otherwise */ + private fun populateAndLayoutMenu(): Boolean { + addMenuOptions(taskContainer) + orientAroundTaskView(taskContainer) + return true + } + + private fun addMenuOptions(taskContainer: TaskContainer) { + if (enableOverviewIconMenu()) { + removeView(taskName) + } else { + taskName.text = TaskUtils.getTitle(context, taskContainer.task) + taskName.setOnClickListener { close(true) } + } + TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer) + .forEach(Consumer { menuOption: SystemShortcut<*> -> this.addMenuOption(menuOption) }) + } + + private fun addMenuOption(menuOption: SystemShortcut<*>) { + val menuOptionView = + recentsViewContainer.layoutInflater.inflate(R.layout.task_view_menu_option, this, false) + as LinearLayout + if (enableOverviewIconMenu()) { + menuOptionView.background = + ResourcesCompat.getDrawable( + resources, + R.drawable.app_chip_menu_item_bg, + context.theme, + ) + } + menuOption.setIconAndLabelFor( + menuOptionView.findViewById(R.id.icon), + menuOptionView.findViewById(R.id.text), + ) + val lp = menuOptionView.layoutParams as LayoutParams + taskView.pagedOrientationHandler.setLayoutParamsForTaskMenuOptionItem( + lp, + menuOptionView, + recentsViewContainer.deviceProfile, + ) + // Set an onClick listener on each menu option. The onClick method is responsible for + // ending LiveTile mode on the thumbnail if needed. + menuOptionView.setOnClickListener { v: View? -> menuOption.onClick(v) } + optionLayout.addView(menuOptionView) + } + + private fun orientAroundTaskView(taskContainer: TaskContainer) { + val recentsView = recentsViewContainer.getOverviewPanel>() + orientationHandler = recentsView.pagedOrientationHandler + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + + // Get Position + val deviceProfile = recentsViewContainer.deviceProfile + recentsViewContainer.dragLayer.getDescendantRectRelativeToSelf( + if (enableOverviewIconMenu()) iconView.findViewById(R.id.icon_view_menu_anchor) + else taskContainer.snapshotView, + tempRect, + ) + val insets = recentsViewContainer.dragLayer.insets + val params = layoutParams as BaseDragLayer.LayoutParams + params.width = + orientationHandler.getTaskMenuWidth( + taskContainer.snapshotView, + deviceProfile, + taskContainer.stagePosition, + ) + // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start + params.gravity = + if (enableOverviewIconMenu() && orientationHandler.isLayoutNaturalToLauncher) + Gravity.START + else Gravity.LEFT + layoutParams = params + scaleX = taskView.scaleX + scaleY = taskView.scaleY + + // Set divider spacing + val divider = ShapeDrawable(RectShape()) + divider.paint.color = resources.getColor(android.R.color.transparent) + val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt() + optionLayout.showDividers = + if (enableOverviewIconMenu()) SHOW_DIVIDER_NONE else SHOW_DIVIDER_MIDDLE + + optionLayout.background = + if (enableOverviewIconMenu()) { + ResourcesCompat.getDrawable(resources, R.drawable.app_chip_menu_bg, context.theme) + } else { + null + } + if (enableOverviewIconMenu()) { + background = + ResourcesCompat.getDrawable(resources, R.drawable.app_chip_menu_bg, context.theme) + } + + orientationHandler.setTaskOptionsMenuLayoutOrientation( + deviceProfile, + optionLayout, + dividerSpacing, + divider, + ) + + val thumbnailAlignedX = + if ( + enableOverviewIconMenu() && + orientationHandler.isLayoutNaturalToLauncher && + isLayoutRtl + ) + -(recentsViewContainer.dragLayer.width - tempRect.right - insets.right).toFloat() + else (tempRect.left - insets.left).toFloat() + val thumbnailAlignedY = (tempRect.top - insets.top).toFloat() + + // Changing pivot to make computations easier + // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set, + // which would render the X and Y position set here incorrect + pivotX = 0f + pivotY = 0f + rotation = orientationHandler.degreesRotated + + var taskInsetMargin = resources.getDimension(R.dimen.task_card_margin) + if (enableOverviewIconMenu()) { + elevation = resources.getDimension(R.dimen.task_thumbnail_icon_menu_elevation) + taskInsetMargin = 0f + } + + translationX = + orientationHandler.getTaskMenuX( + thumbnailAlignedX, + this.taskContainer.snapshotView, + deviceProfile, + taskInsetMargin, + iconView, + ) + translationY = + orientationHandler.getTaskMenuY( + thumbnailAlignedY, + this.taskContainer.snapshotView, + this.taskContainer.stagePosition, + this, + taskInsetMargin, + iconView, + ) + } + + private fun animateOpen() { + menuTranslationYBeforeOpen = translationY + menuTranslationXBeforeOpen = translationX + animateOpenOrClosed(closing = false) + mIsOpen = true + } + + private val iconView: View + get() = taskContainer.iconView.asView() + + private fun animateOpenOrClosed(closing: Boolean, animated: Boolean = true) { + openCloseAnimator?.let { if (it.isRunning) it.cancel() } + // If we're opening, we just start from the beginning as a new `TaskMenuView` is + // created + // each time we do the open animation so there will never be a partial value + // here. + var revealAnimationStartProgress = 0f + if (closing) revealAnimator?.let { revealAnimationStartProgress = 1f - it.animatedFraction } + revealAnimator = + createOpenCloseOutlineProvider() + .createRevealAnimator(this, closing, revealAnimationStartProgress) + .apply { + interpolator = + if (enableOverviewIconMenu()) Interpolators.EMPHASIZED + else Interpolators.DECELERATE + + if (enableRefactorTaskThumbnail()) { + addUpdateListener { animation: ValueAnimator -> + val animatedFraction = animation.animatedFraction + val openProgress = + if (closing) (1 - animatedFraction) else animatedFraction + taskContainer.updateMenuOpenProgress(openProgress) + } + } + } + openCloseAnimator = + AnimatorSet() + .apply { + duration = + when { + animated && closing -> REVEAL_CLOSE_DURATION + animated && !closing -> REVEAL_OPEN_DURATION + else -> 0L + } + addListener( + object : AnimationSuccessListener() { + override fun onAnimationStart(animation: Animator) { + visibility = VISIBLE + if (closing) onClosingStartCallback?.run() + } + + override fun onAnimationSuccess(animator: Animator) { + if (closing) closeComplete() + } + } + ) + } + .also { animator -> + val animatorBuilder = animator.play(revealAnimator) + if (enableOverviewIconMenu()) { + animateOpenOrCloseAppChip(closing, animatorBuilder) + } else { + animatorBuilder.with( + ObjectAnimator.ofFloat(this, ALPHA, (if (closing) 0 else 1).toFloat()) + ) + } + + if (!enableRefactorTaskThumbnail()) { + animatorBuilder.with( + ObjectAnimator.ofFloat( + taskContainer.thumbnailViewDeprecated, + TaskThumbnailViewDeprecated.DIM_ALPHA, + if (closing) 0f else TaskView.MAX_PAGE_SCRIM_ALPHA, + ) + ) + } + + animator.start() + } + } + + private fun TaskView.isOnGridBottomRow(): Boolean = + (recentsViewContainer.getOverviewPanel() as RecentsView<*, *>).isOnGridBottomRow(this) + + private fun closeComplete() { + mIsOpen = false + recentsViewContainer.dragLayer.removeView(this) + revealAnimator = null + } + + private fun createOpenCloseOutlineProvider(): RoundedRectRevealOutlineProvider { + val radius = TaskCornerRadius.get(mContext) + + val fromRect = + if (enableOverviewIconMenu()) { + Rect( + /* left = */ if (isLayoutRtl) width - iconView.minimumWidth else 0, + /* top = */ 0, + /* right = */ if (isLayoutRtl) width else iconView.minimumWidth, + /* bottom = */ (height * CONTAINER_SCALE_PERCENTAGE).roundToInt(), + ) + } else { + Rect(0, 0, width, 0) + } + + val toRect = Rect(0, 0, width, height) + return RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect) + } + + /** + * Calculates max height based on how much space we have available. If not enough space then the + * view will scroll. The maximum menu size will sit inside the task with a margin on the top and + * bottom. + */ + private fun calculateMaxHeight(): Int = + taskView.pagedOrientationHandler.getTaskMenuHeight( + taskInsetMargin = resources.getDimension(R.dimen.task_card_margin), // taskInsetMargin + deviceProfile = recentsViewContainer.deviceProfile, + taskMenuX = translationX, + taskMenuY = + // Bottom menu can translate up to show more options. So we use the min + // translation allowed to calculate its max height. + if (enableOverviewIconMenu() && taskView.isOnGridBottomRow()) minMenuTop + else translationY, + ) + + private fun setOnClosingStartCallback(onClosingStartCallback: Runnable?) { + this.onClosingStartCallback = onClosingStartCallback + } + + private fun animateOpenOrCloseAppChip(closing: Boolean, animatorBuilder: AnimatorSet.Builder) { + if (!closing) { + alpha = 0f + optionLayout.apply { + pivotX = if (isLayoutRtl) optionLayout.width.toFloat() else 0f + pivotY = 0f + scaleX = CONTAINER_SCALE_PERCENTAGE + scaleY = CONTAINER_SCALE_PERCENTAGE + alpha = 0f + } + } + + val iconAppChip = taskContainer.iconView.asView() as IconAppChipView + + // Animate menu up for enough room to display full menu when task on bottom row. + var additionalTranslationY = 0f + val translationYMargin = orientationHandler.getAppChipMenuMarginY(iconAppChip, isLayoutRtl) + if (taskView.isOnGridBottomRow()) { + val expandedMenuPosition = menuTranslationYBeforeOpen + translationYMargin + val currentMenuBottom: Float = expandedMenuPosition + height + additionalTranslationY = + if (currentMenuBottom < maxMenuBottom) 0f + // Translate menu up for enough room to display full menu when task on bottom row. + else maxMenuBottom - currentMenuBottom + + val currentMenuTop = expandedMenuPosition + additionalTranslationY + // If it translate above the min accepted, it translates to the top of the screen + if (currentMenuTop < minMenuTop) { + // It subtracts the menuTranslation to make it 0 (top of the screen) + chip size. + additionalTranslationY = -expandedMenuPosition + minMenuTop + } + } + + val translationYAnim = + ObjectAnimator.ofFloat( + this, + TRANSLATION_Y, + if (closing) menuTranslationYBeforeOpen + else menuTranslationYBeforeOpen + translationYMargin + additionalTranslationY, + ) + translationYAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(translationYAnim) + + val menuTranslationYAnim: ObjectAnimator = + ObjectAnimator.ofFloat( + iconAppChip.getMenuTranslationY(), + MultiPropertyFactory.MULTI_PROPERTY_VALUE, + if (closing) 0f else additionalTranslationY, + ) + menuTranslationYAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(menuTranslationYAnim) + + var additionalTranslationX = 0f + if ( + taskView.pagedOrientationHandler.isLayoutNaturalToLauncher && + taskContainer.stagePosition == + SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT + ) { + // Animate menu and icon when split task would display off the side of the screen. + additionalTranslationX = + max( + (translationX + width - + (recentsViewContainer.deviceProfile.deviceProperties.widthPx - + resources.getDimensionPixelSize( + R.dimen.task_menu_edge_padding + ) * 2)) + .toDouble(), + 0.0, + ) + .toFloat() + } + + val translationXMargin = orientationHandler.getAppChipMenuMarginX(iconAppChip, isLayoutRtl) + val translationXAnim = + ObjectAnimator.ofFloat( + this, + TRANSLATION_X, + if (closing) menuTranslationXBeforeOpen + else menuTranslationXBeforeOpen - translationXMargin - additionalTranslationX, + ) + translationXAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(translationXAnim) + + val menuTranslationXAnim: ObjectAnimator = + ObjectAnimator.ofFloat( + iconAppChip.getMenuTranslationX(), + MultiPropertyFactory.MULTI_PROPERTY_VALUE, + if (closing) 0f else -additionalTranslationX, + ) + menuTranslationXAnim.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(menuTranslationXAnim) + + // Scaling the container inside the menu + val toScaleX = if (closing) CONTAINER_SCALE_PERCENTAGE else 1f + val animatorScaleX = ObjectAnimator.ofFloat(optionLayout, SCALE_X, toScaleX) + animatorScaleX.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(animatorScaleX) + + val toScaleY = if (closing) CONTAINER_SCALE_PERCENTAGE else 1f + val animatorScaleY = ObjectAnimator.ofFloat(optionLayout, SCALE_Y, toScaleY) + animatorScaleY.interpolator = Interpolators.EMPHASIZED + animatorBuilder.with(animatorScaleY) + + val alphaValue = if (closing) 0f else 1f + val optionLayoutAlphaAnimator = + ObjectAnimator.ofFloat(optionLayout, ALPHA, alphaValue).apply { + interpolator = TimeInterpolator { + clampToProgress(Interpolators.EMPHASIZED.getInterpolation(it), .75f, 1f) + } + } + animatorBuilder.with(optionLayoutAlphaAnimator) + + val menuAlphaAnimator = + ObjectAnimator.ofFloat(this, ALPHA, alphaValue).apply { + interpolator = TimeInterpolator { + clampToProgress(Interpolators.EMPHASIZED.getInterpolation(it), .48f, .74f) + } + } + animatorBuilder.with(menuAlphaAnimator) + + val recentsView = recentsViewContainer.getOverviewPanel>() + val isAnimated = !recentsView.isSplitSelectionActive + animatorBuilder.with(iconAppChip.revealAnim(isRevealing = !closing, isAnimated)) + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (enableOverviewIconMenu()) { + if (event.action != KeyEvent.ACTION_DOWN) return super.dispatchKeyEvent(event) + + val isFirstMenuOptionFocused = optionLayout.indexOfChild(optionLayout.focusedChild) == 0 + val isLastMenuOptionFocused = + optionLayout.indexOfChild(optionLayout.focusedChild) == optionLayout.childCount - 1 + if ( + (isLastMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_DOWN) || + (isFirstMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_UP) + ) { + iconView.requestFocus() + return true + } else { + val currentFocus = findFocus() ?: return super.dispatchKeyEvent(event) + + val nextFocus = + when (event.keyCode) { + KeyEvent.KEYCODE_DPAD_UP -> focusSearch(currentFocus, FOCUS_BACKWARD) + KeyEvent.KEYCODE_DPAD_DOWN -> focusSearch(currentFocus, FOCUS_FORWARD) + KeyEvent.KEYCODE_TAB -> + focusSearch( + currentFocus, + if (event.isShiftPressed) FOCUS_BACKWARD else FOCUS_FORWARD, + ) + else -> null + } + + return nextFocus?.requestFocus() ?: super.dispatchKeyEvent(event) + } + } + return super.dispatchKeyEvent(event) + } + + companion object { + private val REVEAL_OPEN_DURATION = if (enableOverviewIconMenu()) 417L else 150L + private val REVEAL_CLOSE_DURATION = if (enableOverviewIconMenu()) 333L else 100L + private const val CONTAINER_SCALE_PERCENTAGE = .8f + + /** Show a task menu for the given taskContainer. */ + /** Show a task menu for the given taskContainer. */ + @JvmOverloads + fun showForTask( + taskContainer: TaskContainer, + onClosingStartCallback: Runnable? = null, + ): Boolean { + val container: RecentsViewContainer = + RecentsViewContainer.containerFromContext(taskContainer.taskView.context) + val taskMenuView = + container.layoutInflater.inflate(R.layout.task_menu, container.dragLayer, false) + as TaskMenuView + taskMenuView.setOnClosingStartCallback(onClosingStartCallback) + return taskMenuView.populateAndShowForTask(taskContainer) + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt index 346eb2ff65..78730ec3c7 100644 --- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt +++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt @@ -30,7 +30,6 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import app.lawnchair.theme.color.tokens.ColorTokens -import com.android.launcher3.BaseDraggingActivity import com.android.launcher3.DeviceProfile import com.android.launcher3.InsettableFrameLayout import com.android.launcher3.R @@ -39,13 +38,16 @@ import com.android.launcher3.popup.RoundedArrowDrawable import com.android.launcher3.popup.SystemShortcut import com.android.launcher3.util.Themes import com.android.quickstep.TaskOverlayFactory -import com.android.quickstep.views.TaskView.TaskContainer class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T : Context { companion object { const val TAG = "TaskMenuViewWithArrow" - fun showForTask(taskContainer: TaskContainer, alignedOptionIndex: Int = 0): Boolean { + fun showForTask( + taskContainer: TaskContainer, + alignedOptionIndex: Int = 0, + onClosedCallback: Runnable? = null + ): Boolean { val container: RecentsViewContainer = RecentsViewContainer.containerFromContext(taskContainer.taskView.context) val taskMenuViewWithArrow = @@ -55,12 +57,18 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T false ) as TaskMenuViewWithArrow<*> - return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex) + return taskMenuViewWithArrow.populateAndShowForTask( + taskContainer, + alignedOptionIndex, + onClosedCallback + ) } } constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor( context: Context, attrs: AttributeSet, @@ -82,6 +90,7 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T private var alignedOptionIndex: Int = 0 private val extraSpaceForRowAlignment: Int get() = optionMeasuredHeight * alignedOptionIndex + private val menuPaddingEnd = context.resources.getDimensionPixelSize(R.dimen.task_card_margin) private lateinit var taskView: TaskView @@ -91,13 +100,14 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T private var optionMeasuredHeight = 0 private val arrowHorizontalPadding: Int get() = - if (taskView.isFocusedTask) + if (taskView.isLargeTile) resources.getDimensionPixelSize(R.dimen.task_menu_horizontal_padding) else 0 private var iconView: IconView? = null private var scrim: View? = null private val scrimAlpha = 0.8f + private var onClosedCallback: Runnable? = null override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0 @@ -141,7 +151,8 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T private fun populateAndShowForTask( taskContainer: TaskContainer, - alignedOptionIndex: Int + alignedOptionIndex: Int, + onClosedCallback: Runnable? ): Boolean { if (isAttachedToWindow) { return false @@ -150,6 +161,7 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T taskView = taskContainer.taskView this.taskContainer = taskContainer this.alignedOptionIndex = alignedOptionIndex + this.onClosedCallback = onClosedCallback if (!populateMenu()) return false addScrim() show() @@ -252,6 +264,7 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T super.closeComplete() popupContainer.removeView(scrim) popupContainer.removeView(iconView) + onClosedCallback?.run() } /** @@ -263,8 +276,8 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T IconView(context).apply { layoutParams = FrameLayout.LayoutParams( - taskContainer.iconView.width, - taskContainer.iconView.height + taskContainer.iconView.asView().width, + taskContainer.iconView.asView().height ) x = mTempRect.left.toFloat() - insets.left y = mTempRect.top.toFloat() - insets.top diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java index 4283d0e4cf..859feb2ec5 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java @@ -28,16 +28,13 @@ import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; -import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; -import android.os.Build; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Property; @@ -45,18 +42,16 @@ import android.view.View; import android.widget.ImageView; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.core.graphics.ColorUtils; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; -import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags; import com.android.launcher3.util.ViewPool; +import com.android.quickstep.FullscreenDrawParams; import com.android.quickstep.TaskOverlayFactory.TaskOverlay; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.views.TaskView.FullscreenDrawParams; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; @@ -70,8 +65,6 @@ import java.util.Objects; */ @Deprecated public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusable { - private static final MainThreadInitializedObject TEMP_PARAMS = - new MainThreadInitializedObject<>(FullscreenDrawParams::new); public static final Property DIM_ALPHA = new FloatProperty("dimAlpha") { @@ -99,36 +92,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab } }; - /** Use to animate thumbnail translationX while first app in split selection is initiated */ - public static final Property SPLIT_SELECT_TRANSLATE_X = - new FloatProperty("splitSelectTranslateX") { - @Override - public void setValue(TaskThumbnailViewDeprecated thumbnail, - float splitSelectTranslateX) { - thumbnail.applySplitSelectTranslateX(splitSelectTranslateX); - } - - @Override - public Float get(TaskThumbnailViewDeprecated thumbnailView) { - return thumbnailView.mSplitSelectTranslateX; - } - }; - - /** Use to animate thumbnail translationY while first app in split selection is initiated */ - public static final Property SPLIT_SELECT_TRANSLATE_Y = - new FloatProperty("splitSelectTranslateY") { - @Override - public void setValue(TaskThumbnailViewDeprecated thumbnail, - float splitSelectTranslateY) { - thumbnail.applySplitSelectTranslateY(splitSelectTranslateY); - } - - @Override - public Float get(TaskThumbnailViewDeprecated thumbnailView) { - return thumbnailView.mSplitSelectTranslateY; - } - }; - private final RecentsViewContainer mContainer; private TaskOverlay mOverlay; private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -141,9 +104,10 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. private final Rect mPreviewRect = new Rect(); private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper(); - private TaskView.FullscreenDrawParams mFullscreenParams; + private FullscreenDrawParams mFullscreenParams; private ImageView mSplashView; private Drawable mSplashViewDrawable; + private TaskView mTaskView; @Nullable private Task mTask; @@ -160,8 +124,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab private boolean mOverlayEnabled; /** Used as a placeholder when the original thumbnail animates out to. */ private boolean mShowSplashForSplitSelection; - private float mSplitSelectTranslateX; - private float mSplitSelectTranslateY; public TaskThumbnailViewDeprecated(Context context) { this(context, null); @@ -180,8 +142,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mContainer = RecentsViewContainer.containerFromContext(context); // Initialize with placeholder value. It is overridden later by TaskView - mFullscreenParams = TEMP_PARAMS.get(context); - + mFullscreenParams = new FullscreenDrawParams(context, __ -> 0f, __ -> 0f); mDimColor = RecentsView.getForegroundScrimDimColor(context); mDimmingPaintAfterClearing.setColor(mDimColor); } @@ -189,10 +150,11 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab /** * Updates the thumbnail to draw the provided task */ - public void bind(Task task, TaskOverlay overlay) { + public void bind(Task task, TaskOverlay overlay, TaskView taskView) { mOverlay = overlay; mOverlay.reset(); mTask = task; + mTaskView = taskView; int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; mPaint.setColor(color); mBackgroundPaint.setColor(color); @@ -200,17 +162,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab updateSplashView(mTask.icon); } - /** - * Sets TaskOverlay without binding a task. - * - * @deprecated Should only be used when using new - * {@link com.android.quickstep.task.thumbnail.TaskThumbnailView}. - */ - @Deprecated - public void setTaskOverlay(TaskOverlay overlay) { - mOverlay = overlay; - } - /** * Updates the thumbnail. * @@ -298,40 +249,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab return mDimAlpha; } - /** - * Get the scaled insets that are being used to draw the task view. This is a subsection of - * the full snapshot. - * - * @return the insets in snapshot bitmap coordinates. - */ - @RequiresApi(api = Build.VERSION_CODES.Q) - public Insets getScaledInsets() { - if (mThumbnailData == null) { - return Insets.NONE; - } - - RectF bitmapRect = new RectF( - 0, - 0, - mThumbnailData.getThumbnail().getWidth(), - mThumbnailData.getThumbnail().getHeight()); - RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()); - - // The position helper matrix tells us how to transform the bitmap to fit the view, the - // inverse tells us where the view would be in the bitmaps coordinates. The insets are the - // difference between the bitmap bounds and the projected view bounds. - Matrix boundsToBitmapSpace = new Matrix(); - mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace); - RectF boundsInBitmapSpace = new RectF(); - boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect); - - DeviceProfile dp = mContainer.getDeviceProfile(); - int bottomInset = dp.isTablet - ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0; - return Insets.of(0, 0, 0, bottomInset); - } - - @SystemUiControllerFlags public int getSysUiStatusNavFlags() { if (mThumbnailData != null) { @@ -358,7 +275,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab canvas.save(); // Draw the insets if we're being drawn fullscreen (we do this for quick switch). drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), - mFullscreenParams.getCurrentDrawnCornerRadius()); + mFullscreenParams.getCurrentCornerRadius()); canvas.restore(); } @@ -366,15 +283,15 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab return mPreviewPositionHelper; } - public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { + public void setFullscreenParams(FullscreenDrawParams fullscreenParams) { mFullscreenParams = fullscreenParams; invalidate(); } public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius) { - if (mTask != null && getTaskView().isRunningTask() - && !getTaskView().getShouldShowScreenshot()) { + if (mTask != null && mTaskView.isRunningTask() + && !mTaskView.getShouldShowScreenshot()) { canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint); canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mDimmingPaintAfterClearing); @@ -415,35 +332,6 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab } } - /** See {@link #SPLIT_SELECT_TRANSLATE_X} */ - protected void applySplitSelectTranslateX(float splitSelectTranslateX) { - mSplitSelectTranslateX = splitSelectTranslateX; - applyTranslateX(); - } - - /** See {@link #SPLIT_SELECT_TRANSLATE_Y} */ - protected void applySplitSelectTranslateY(float splitSelectTranslateY) { - mSplitSelectTranslateY = splitSelectTranslateY; - applyTranslateY(); - } - - private void applyTranslateX() { - setTranslationX(mSplitSelectTranslateX); - } - - private void applyTranslateY() { - setTranslationY(mSplitSelectTranslateY); - } - - protected void resetViewTransforms() { - mSplitSelectTranslateX = 0; - mSplitSelectTranslateY = 0; - } - - public TaskView getTaskView() { - return (TaskView) getParent(); - } - public void setOverlayEnabled(boolean overlayEnabled) { if (mOverlayEnabled != overlayEnabled) { mOverlayEnabled = overlayEnabled; @@ -496,9 +384,9 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab float viewCenterY = viewHeight / 2f; float centeredDrawableLeft = (viewWidth - drawableWidth) / 2f; float centeredDrawableTop = (viewHeight - drawableHeight) / 2f; - float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale(); - float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null - ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen(); + float nonGridScale = mTaskView == null ? 1 : 1 / mTaskView.getNonGridScale(); + float recentsMaxScale = mTaskView == null || mTaskView.getRecentsView() == null + ? 1 : 1 / mTaskView.getRecentsView().getMaxScaleForFullScreen(); float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX()); float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY()); @@ -524,8 +412,13 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab thumbnailDataAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT); } - private boolean isThumbnailRotationDifferentFromTask() { - RecentsView recents = getTaskView().getRecentsView(); + /** + * Returns whether or not the current thumbnail is a different orientation to the task. + *

+ * Used to disable modal state when screenshot doesn't match the device orientation. + */ + public boolean isThumbnailRotationDifferentFromTask() { + RecentsView recents = mTaskView.getRecentsView(); if (recents == null || mThumbnailData == null) { return false; } @@ -544,7 +437,9 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab */ private void refreshOverlay() { if (mOverlayEnabled) { - mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.getMatrix(), + mOverlay.initOverlay(mTask, + mThumbnailData != null ? mThumbnailData.getThumbnail() : null, + mPreviewPositionHelper.getMatrix(), mPreviewPositionHelper.isOrientationChanged()); } else { mOverlay.reset(); @@ -571,15 +466,15 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab if (mBitmapShader != null && mThumbnailData != null) { mPreviewRect.set(0, 0, mThumbnailData.getThumbnail().getWidth(), mThumbnailData.getThumbnail().getHeight()); - int currentRotation = getTaskView().getOrientedState().getRecentsActivityRotation(); + int currentRotation = mTaskView.getOrientedState().getRecentsActivityRotation(); boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, - getMeasuredWidth(), getMeasuredHeight(), dp.isTablet, currentRotation, isRtl); + getMeasuredWidth(), getMeasuredHeight(), dp.getDeviceProperties().isTablet(), currentRotation, isRtl); mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix()); mPaint.setShader(mBitmapShader); } - getTaskView().updateCurrentFullscreenParams(); + mTaskView.updateFullscreenParams(); invalidate(); } @@ -617,6 +512,10 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab return mThumbnailData.isRealSnapshot && !mTask.isLocked; } + public Matrix getThumbnailMatrix() { + return mPreviewPositionHelper.getMatrix(); + } + @Override public void onRecycle() { // Do nothing diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index 3b2be643ec..1364821676 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -22,7 +22,6 @@ import android.animation.ObjectAnimator import android.annotation.IdRes import android.app.ActivityOptions import android.content.Context -import android.content.Intent import android.graphics.Canvas import android.graphics.PointF import android.graphics.Rect @@ -39,7 +38,6 @@ import android.view.View.OnClickListener import android.view.ViewGroup import android.view.ViewStub import android.view.accessibility.AccessibilityNodeInfo -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.FrameLayout import android.widget.Toast import androidx.annotation.IntDef @@ -47,63 +45,77 @@ import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import androidx.core.view.updateLayoutParams import com.android.app.animation.Interpolators +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.Flags.enableCoroutineThreadingImprovements import com.android.launcher3.Flags.enableCursorHoverStates -import com.android.launcher3.Flags.enableFocusOutline -import com.android.launcher3.Flags.enableGridOnlyOverview -import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableDesktopExplodedView +import com.android.launcher3.Flags.enableLargeDesktopWindowingTile +import com.android.launcher3.Flags.enableRefactorDigitalWellbeingToast +import com.android.launcher3.Flags.enableRefactorTaskContentView import com.android.launcher3.Flags.enableRefactorTaskThumbnail -import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag -import com.android.launcher3.LauncherSettings import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.anim.AnimatedFloat -import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH import com.android.launcher3.logging.StatsLogManager.LauncherEvent import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.model.data.ItemInfoWithIcon -import com.android.launcher3.model.data.WorkspaceItemInfo -import com.android.launcher3.pm.UserCache +import com.android.launcher3.model.data.TaskViewItemInfo import com.android.launcher3.testing.TestLogging import com.android.launcher3.testing.shared.TestProtocol import com.android.launcher3.util.CancellableTask -import com.android.launcher3.util.DisplayController import com.android.launcher3.util.Executors +import com.android.launcher3.util.KFloatProperty +import com.android.launcher3.util.MultiPropertyDelegate import com.android.launcher3.util.MultiPropertyFactory -import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE +import com.android.launcher3.util.MultiValueAlpha +import com.android.launcher3.util.OverviewReleaseFlags.enableGridOnlyOverview +import com.android.launcher3.util.OverviewReleaseFlags.enableOverviewIconMenu import com.android.launcher3.util.RunnableList -import com.android.launcher3.util.SafeCloseable -import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED -import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption import com.android.launcher3.util.SplitConfigurationOptions.StagePosition import com.android.launcher3.util.TraceHelper import com.android.launcher3.util.TransformingTouchDelegate import com.android.launcher3.util.ViewPool +import com.android.launcher3.util.coroutines.DispatcherProvider import com.android.launcher3.util.rects.set -import com.android.launcher3.views.ActivityContext +import com.android.quickstep.FullscreenDrawParams import com.android.quickstep.RecentsModel import com.android.quickstep.RemoteAnimationTargets -import com.android.quickstep.TaskAnimationManager +import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle import com.android.quickstep.TaskOverlayFactory -import com.android.quickstep.TaskOverlayFactory.TaskOverlay -import com.android.quickstep.TaskUtils import com.android.quickstep.TaskViewUtils +import com.android.quickstep.fallback.window.RecentsWindowFlags.enableOverviewOnConnectedDisplays import com.android.quickstep.orientation.RecentsPagedOrientationHandler -import com.android.quickstep.task.thumbnail.TaskThumbnail -import com.android.quickstep.task.thumbnail.TaskThumbnailView -import com.android.quickstep.task.viewmodel.TaskViewData +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.recents.di.get +import com.android.quickstep.recents.di.inject +import com.android.quickstep.recents.domain.usecase.ThumbnailPosition +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState +import com.android.quickstep.recents.ui.viewmodel.TaskViewModel +import com.android.quickstep.task.thumbnail.TaskContentView import com.android.quickstep.util.ActiveGestureErrorDetector import com.android.quickstep.util.ActiveGestureLog import com.android.quickstep.util.BorderAnimator import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator +import com.android.quickstep.util.GroupTask import com.android.quickstep.util.RecentsOrientedState +import com.android.quickstep.util.SingleTask import com.android.quickstep.util.TaskCornerRadius import com.android.quickstep.util.TaskRemovedDuringLaunchListener +import com.android.quickstep.util.isExternalDisplay +import com.android.quickstep.util.safeDisplayId +import com.android.quickstep.views.IconAppChipView.AppChipStatus +import com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL +import com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED import com.android.quickstep.views.RecentsView.UNBOUND_TASK_VIEW_ID import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData import com.android.systemui.shared.system.ActivityManagerWrapper -import com.android.systemui.shared.system.QuickStepContract +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** A task in the Recents view. */ open class TaskView @@ -114,7 +126,9 @@ constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0, focusBorderAnimator: BorderAnimator? = null, - hoverBorderAnimator: BorderAnimator? = null + hoverBorderAnimator: BorderAnimator? = null, + val type: TaskViewType = TaskViewType.SINGLE, + protected val thumbnailFullscreenParams: FullscreenDrawParams = FullscreenDrawParams(context), ) : FrameLayout(context, attrs), ViewPool.Reusable { /** * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which @@ -124,37 +138,43 @@ constructor( @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS) annotation class TaskDataChanges - /** Type of task view */ - @Retention(AnnotationRetention.SOURCE) - @IntDef(Type.SINGLE, Type.GROUPED, Type.DESKTOP) - annotation class Type { - companion object { - const val SINGLE = 1 - const val GROUPED = 2 - const val DESKTOP = 3 - } - } + var groupTask: GroupTask? = null - val taskViewData = TaskViewData() val taskIds: IntArray /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */ get() = taskContainers.map { it.task.key.id }.toIntArray() - val thumbnailViews: Array - get() = taskContainers.map { it.thumbnailViewDeprecated }.toTypedArray() + val taskIdSet: Set + /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */ + get() = taskContainers.map { it.task.key.id }.toSet() + + val snapshotViews: Array + get() = taskContainers.map { it.snapshotView }.toTypedArray() + + val taskContentViews: Array + get() = taskContainers.map { it.taskContentView }.toTypedArray() val isGridTask: Boolean /** Returns whether the task is part of overview grid and not being focused. */ - get() = container.deviceProfile.isTablet && !isFocusedTask + get() = container.deviceProfile.getDeviceProperties().isTablet && !isLargeTile val isRunningTask: Boolean get() = this === recentsView?.runningTaskView - val isFocusedTask: Boolean - get() = this === recentsView?.focusedTaskView + private val isSelectedTask: Boolean + get() = this === recentsView?.selectedTaskView - val taskCornerRadius: Float - get() = currentFullscreenParams.cornerRadius + open val displayId: Int + get() = taskContainers.firstOrNull()?.task.safeDisplayId + + val isExternalDisplay: Boolean + get() = displayId.isExternalDisplay + + val isLargeTile: Boolean + get() = + this == recentsView?.focusedTaskView || + (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP) || + (isExternalDisplay && !enableOverviewOnConnectedDisplays()) val recentsView: RecentsView<*, *>? get() = parent as? RecentsView<*, *> @@ -162,21 +182,28 @@ constructor( val pagedOrientationHandler: RecentsPagedOrientationHandler get() = orientedState.orientationHandler - @get:Deprecated("Use [taskContainers] instead.") - val firstTask: Task + val firstTaskContainer: TaskContainer? + get() = taskContainers.firstOrNull() + + val firstTask: Task? /** Returns the first task bound to this TaskView. */ - get() = taskContainers[0].task + get() = firstTaskContainer?.task - @get:Deprecated("Use [taskContainers] instead.") - val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated - /** Returns the first thumbnailView of the TaskView. */ - get() = taskContainers[0].thumbnailViewDeprecated + val firstItemInfo: ItemInfo? + get() = firstTaskContainer?.itemInfo - @get:Deprecated("Use [taskContainers] instead.") - val firstItemInfo: ItemInfo - get() = taskContainers[0].itemInfo + val isOnGridBottomRow: Boolean + get() = recentsView?.isOnGridBottomRow(this) == true + + /** + * A [TaskViewItemInfo] of this TaskView. The [firstTaskContainer] will be used to get some + * specific information like user, title etc of the Task. However, these task specific + * information will be skipped if the TaskView has no [taskContainers]. Note, please use + * [TaskContainer.itemInfo] for [TaskViewItemInfo] on a specific [TaskContainer]. + */ + val itemInfo: TaskViewItemInfo + get() = TaskViewItemInfo(this, firstTaskContainer) - private val currentFullscreenParams = FullscreenDrawParams(context) protected val container: RecentsViewContainer = RecentsViewContainer.containerFromContext(context) protected val lastTouchDownPosition = PointF() @@ -194,12 +221,9 @@ constructor( * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does * not change according to a temporary state (e.g. task offset). */ - get() = - (getNonGridTrans(nonGridTranslationX) + - getGridTrans(this.gridTranslationX) + - getNonGridTrans(nonGridPivotTranslationX)) + get() = (getNonGridTrans(nonGridTranslationX) + getGridTrans(this.gridTranslationX)) - protected val persistentTranslationY: Float + val persistentTranslationY: Float /** * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does * not change according to a temporary state (e.g. task offset). @@ -210,21 +234,21 @@ constructor( get() = pagedOrientationHandler.getPrimaryValue( SPLIT_SELECT_TRANSLATION_X, - SPLIT_SELECT_TRANSLATION_Y + SPLIT_SELECT_TRANSLATION_Y, ) protected val secondarySplitTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue( SPLIT_SELECT_TRANSLATION_X, - SPLIT_SELECT_TRANSLATION_Y + SPLIT_SELECT_TRANSLATION_Y, ) - protected val primaryDismissTranslationProperty: FloatProperty + val primaryDismissTranslationProperty: FloatProperty get() = pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) - protected val secondaryDismissTranslationProperty: FloatProperty + val secondaryDismissTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) @@ -232,26 +256,61 @@ constructor( get() = pagedOrientationHandler.getPrimaryValue( TASK_OFFSET_TRANSLATION_X, - TASK_OFFSET_TRANSLATION_Y + TASK_OFFSET_TRANSLATION_Y, ) protected val secondaryTaskOffsetTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue( TASK_OFFSET_TRANSLATION_X, - TASK_OFFSET_TRANSLATION_Y + TASK_OFFSET_TRANSLATION_Y, ) protected val taskResistanceTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue( TASK_RESISTANCE_TRANSLATION_X, - TASK_RESISTANCE_TRANSLATION_Y + TASK_RESISTANCE_TRANSLATION_Y, ) private val tempCoordinates = FloatArray(2) - private val focusBorderAnimator: BorderAnimator? - private val hoverBorderAnimator: BorderAnimator? + private val focusBorderAnimator: BorderAnimator? = + focusBorderAnimator + ?: createSimpleBorderAnimator( + TaskCornerRadius.get(context).toInt(), + context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width), + this::getThumbnailBounds, + this, + context + .obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes) + .getColor( + R.styleable.TaskView_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + + private val hoverBorderAnimator: BorderAnimator? = + hoverBorderAnimator + ?: if (enableCursorHoverStates()) + createSimpleBorderAnimator( + TaskCornerRadius.get(context).toInt(), + context.resources.getDimensionPixelSize(R.dimen.task_hover_border_width), + this::getThumbnailBounds, + this, + context + .obtainStyledAttributes( + attrs, + R.styleable.TaskView, + defStyleAttr, + defStyleRes, + ) + .getColor( + R.styleable.TaskView_hoverBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR, + ), + ) + else null + private val rootViewDisplayId: Int get() = rootView.display?.displayId ?: Display.DEFAULT_DISPLAY @@ -263,11 +322,21 @@ constructor( var taskViewId = UNBOUND_TASK_VIEW_ID var isEndQuickSwitchCuj = false + var isBeingDraggedForDismissal = false + val isBeingDismissed + get() = secondaryDismissTranslationProperty.get(this) != 0f + + var sysUiStatusNavFlags: Int = 0 + get() = + if (enableRefactorTaskThumbnail()) field + else firstTaskContainer?.thumbnailViewDeprecated?.sysUiStatusNavFlags ?: 0 + private set // Various animation progress variables. // progress: 0 = show icon and no insets; 1 = don't show icon and show full insets. protected var fullscreenProgress = 0f set(value) { + if (value == field && enableOverviewIconMenu()) return field = Utilities.boundToRange(value, 0f, 1f) onFullscreenProgressChanged(field) } @@ -292,6 +361,18 @@ constructor( onModalnessUpdated(field) } + var modalPivot: PointF? = null + set(value) { + field = value + updatePivots() + } + + var splitSplashAlpha = 0f + set(value) { + field = value + applyThumbnailSplashAlpha() + } + protected var taskThumbnailSplashAlpha = 0f set(value) { field = value @@ -310,6 +391,12 @@ constructor( applyScale() } + var modalScale = 1f + set(value) { + field = value + applyScale() + } + private var dismissTranslationX = 0f set(value) { field = value @@ -354,7 +441,7 @@ constructor( } // The following grid translations scales with mGridProgress. - protected var gridTranslationX = 0f + var gridTranslationX = 0f set(value) { field = value applyTranslationX() @@ -375,13 +462,7 @@ constructor( // Applied as a complement to gridTranslation, for adjusting the carousel overview and quick // switch. - protected var nonGridTranslationX = 0f - set(value) { - field = value - applyTranslationX() - } - - protected var nonGridPivotTranslationX = 0f + var nonGridTranslationX = 0f set(value) { field = value applyTranslationX() @@ -400,14 +481,15 @@ constructor( applyTranslationX() } - protected var stableAlpha = 1f - set(value) { - field = value - alpha = stableAlpha - } + private val taskViewAlpha = MultiValueAlpha(this, Alpha.entries.size) + protected var stableAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Stable) + var attachAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Attach) + var splitAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Split) + private var modalAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.Modal) protected var shouldShowScreenshot = false get() = !isRunningTask || field + private set /** Enable or disable showing border on hover and focus change */ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @@ -423,37 +505,90 @@ constructor( focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) } - private var focusTransitionProgress = 1f + /** + * Used to cache the hover border state so we don't repeatedly call the border animator with + * every hover event when the user hasn't crossed the threshold of the [thumbnailBounds]. + */ + private var hoverBorderVisible = false set(value) { + if (field == value) { + return + } field = value - onFocusTransitionProgressUpdated(field) + Log.d( + TAG, + "${taskIds.contentToString()} - setting border animator visibility to: $field", + ) + hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true) } - private val focusTransitionPropertyFactory = + // Used to cache thumbnail bounds to avoid recalculating on every hover move. + private var thumbnailBounds = Rect() + + // Progress variable indicating if the TaskView is in a settled state: + // 0 = The TaskView is in a transitioning state e.g. during gesture, in quickswitch carousel, + // becoming focus task etc. + // 1 = The TaskView is settled and no longer transitioning + private var settledProgress = 1f + set(value) { + if (value == field && enableOverviewIconMenu()) return + field = value + onSettledProgressUpdated(field) + } + + private val settledProgressPropertyFactory = MultiPropertyFactory( this, - FOCUS_TRANSITION, - FOCUS_TRANSITION_INDEX_COUNT, + SETTLED_PROGRESS, + SettledProgress.entries.size, { x: Float, y: Float -> x * y }, - 1f + 1f, ) - private val focusTransitionFullscreen = - focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN) - private val focusTransitionScaleAndDim = - focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_SCALE_AND_DIM) + private var settledProgressFullscreen by + MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Fullscreen) + private var settledProgressGesture by + MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Gesture) + private var settledProgressDismiss by + MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Dismiss) + + private val viewModel = + if (enableRefactorTaskThumbnail()) { + TaskViewModel( + taskViewType = type, + recentsViewData = RecentsDependencies.get(context), + getTaskUseCase = RecentsDependencies.get(context), + getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(context), + isThumbnailValidUseCase = RecentsDependencies.get(context), + getThumbnailPositionUseCase = RecentsDependencies.get(context), + dispatcherProvider = RecentsDependencies.get(context), + ) + } else null + private val dispatcherProvider: DispatcherProvider by RecentsDependencies.inject() + private val coroutineScope: CoroutineScope by RecentsDependencies.inject() + private val coroutineJobs = mutableListOf() /** - * Returns an animator of [focusTransitionScaleAndDim] that transition out with a built-in + * Returns an animator of [settledProgressDismiss] that transition in with a built-in * interpolator. */ - fun getFocusTransitionScaleAndDimOutAnimator(): ObjectAnimator = + fun getDismissIconFadeInAnimator(): ObjectAnimator = + ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_DISMISS, 1f).apply { + duration = FADE_IN_ICON_DURATION + interpolator = FADE_IN_ICON_INTERPOLATOR + } + + /** + * Returns an animator of [settledProgressDismiss] that transition out with a built-in + * interpolator. [AnimatedFloat] is used to apply another level of interpolation, on top of + * interpolator set to the [Animator] by the caller. + */ + fun getDismissIconFadeOutAnimator(): ObjectAnimator = AnimatedFloat { v -> - focusTransitionScaleAndDim.value = - FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(v) + settledProgressDismiss = SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(v) } .animateToValue(1f, 0f) - private var iconAndDimAnimator: ObjectAnimator? = null + private var iconFadeInOnGestureCompleteAnimator: ObjectAnimator? = null // The current background requests to load the task thumbnail and icon private val pendingThumbnailLoadRequests = mutableListOf>() private val pendingIconLoadRequests = mutableListOf>() @@ -461,56 +596,15 @@ constructor( init { setOnClickListener { _ -> onClick() } - val keyboardFocusHighlightEnabled = - (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline()) - val cursorHoverStatesEnabled = enableCursorHoverStates() - setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled) - val attrs = - context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes) - try { - this.focusBorderAnimator = - focusBorderAnimator - ?: if (keyboardFocusHighlightEnabled) - createSimpleBorderAnimator( - currentFullscreenParams.cornerRadius.toInt(), - context.resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_border_width - ), - { bounds: Rect -> getThumbnailBounds(bounds) }, - this, - attrs.getColor( - R.styleable.TaskView_focusBorderColor, - BorderAnimator.DEFAULT_BORDER_COLOR - ) - ) - else null - this.hoverBorderAnimator = - hoverBorderAnimator - ?: if (cursorHoverStatesEnabled) - createSimpleBorderAnimator( - currentFullscreenParams.cornerRadius.toInt(), - context.resources.getDimensionPixelSize( - R.dimen.task_hover_border_width - ), - { bounds: Rect -> getThumbnailBounds(bounds) }, - this, - attrs.getColor( - R.styleable.TaskView_hoverBorderColor, - BorderAnimator.DEFAULT_BORDER_COLOR - ) - ) - else null - } finally { - attrs.recycle() - } - } + setWillNotDraw(!enableCursorHoverStates()) + } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public override fun onFocusChanged( gainFocus: Boolean, direction: Int, - previouslyFocusedRect: Rect? + previouslyFocusedRect: Rect?, ) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) if (borderEnabled) { @@ -521,21 +615,19 @@ constructor( override fun onHoverEvent(event: MotionEvent): Boolean { if (borderEnabled) { when (event.action) { - MotionEvent.ACTION_HOVER_ENTER -> - hoverBorderAnimator?.setBorderVisibility(visible = true, animated = true) - MotionEvent.ACTION_HOVER_EXIT -> - hoverBorderAnimator?.setBorderVisibility(visible = false, animated = true) + MotionEvent.ACTION_HOVER_ENTER -> { + getThumbnailBounds(thumbnailBounds) + hoverBorderVisible = event.isWithinThumbnailBounds() + } + MotionEvent.ACTION_HOVER_MOVE -> + hoverBorderVisible = event.isWithinThumbnailBounds() + MotionEvent.ACTION_HOVER_EXIT -> hoverBorderVisible = false else -> {} } } return super.onHoverEvent(event) } - // avoid triggering hover event on child elements which would cause HOVER_EXIT for this - // task view - override fun onInterceptHoverEvent(event: MotionEvent) = - if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event) - override fun dispatchTouchEvent(ev: MotionEvent): Boolean { val recentsView = recentsView ?: return false val splitSelectStateController = recentsView.splitSelectController @@ -562,37 +654,72 @@ constructor( super.draw(canvas) } + override fun setLayoutDirection(layoutDirection: Int) { + super.setLayoutDirection(layoutDirection) + if (enableOverviewIconMenu()) { + val deviceLayoutDirection = resources.configuration.layoutDirection + taskContainers.forEach { + (it.iconView as IconAppChipView).layoutDirection = deviceLayoutDirection + } + } + } + @RequiresApi(Build.VERSION_CODES.Q) override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) - val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx - if (container.deviceProfile.isTablet) { - pivotX = (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat() - pivotY = thumbnailTopMargin.toFloat() - } else { - pivotX = (right - left) * 0.5f - pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f - } + updatePivots() systemGestureExclusionRects = SYSTEM_GESTURE_EXCLUSION_RECT.onEach { it.right = width it.bottom = height } + getThumbnailBounds(thumbnailBounds) + } + + private fun updatePivots() { + val modalPivot = modalPivot + if (modalPivot != null) { + pivotX = modalPivot.x + pivotY = modalPivot.y + } else { + val thumbnailTopMargin = + container.deviceProfile.overviewProfile.taskThumbnailTopMarginPx + if (container.deviceProfile.getDeviceProperties().isTablet) { + pivotX = + (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat() + pivotY = thumbnailTopMargin.toFloat() + } else { + pivotX = (right - left) * 0.5f + pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f + } + } } override fun onRecycle() { + isBeingDraggedForDismissal = false resetPersistentViewTransforms() + + groupTask = null + // Bind ViewModel to no taskIds + viewModel?.bind() + attachAlpha = 1f + splitAlpha = 1f + splitSplashAlpha = 0f + modalAlpha = 1f + modalScale = 1f + modalPivot = null + taskThumbnailSplashAlpha = 0f // Clear any references to the thumbnail (it will be re-read either from the cache or the // system on next bind) - if (enableRefactorTaskThumbnail()) { - notifyIsRunningTaskUpdated() - } else { + if (!enableRefactorTaskThumbnail()) { taskContainers.forEach { it.thumbnailViewDeprecated.setThumbnail(it.task, null) } } setOverlayEnabled(false) onTaskListVisibilityChanged(false) borderEnabled = false + hoverBorderVisible = false taskViewId = UNBOUND_TASK_VIEW_ID + // TODO(b/390583187): Clean the components UI State when TaskView is recycled. taskContainers.forEach { it.destroy() } } @@ -602,34 +729,45 @@ constructor( override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(info) with(info) { - addAction( - AccessibilityAction( - R.id.action_close, - context.getText(R.string.accessibility_close) - ) - ) + // Only make actions available if the app icon menu is visible to the user. + // When modalness is >0, the user is in select mode and the icon menu is hidden. + // When split selection is active, they should only be able to select the app and not + // take any other action. + val shouldPopulateAccessibilityMenu = + modalness == 0f && recentsView?.isSplitSelectionActive == false + if (shouldPopulateAccessibilityMenu) { + taskContainers.forEach { + TraceHelper.allowIpcs("TV.a11yInfo") { + TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut + -> + addAction(shortcut.createAccessibilityAction(context)) + } + } + } - taskContainers.forEach { - TraceHelper.allowIpcs("TV.a11yInfo") { - TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut -> - addAction(shortcut.createAccessibilityAction(context)) + // Add DWB accessibility action at the end of the list + taskContainers.forEach { + if ( + enableRefactorDigitalWellbeingToast() && + it.taskContentView is TaskContentView + ) { + it.taskContentView.getSupportedAccessibilityActions().forEach(::addAction) + } else { + it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction) } } } - // Add DWB accessibility action at the end of the list - taskContainers.forEach { - it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction) - } - recentsView?.let { collectionItemInfo = - AccessibilityNodeInfo.CollectionItemInfo.obtain( + AccessibilityNodeInfo.CollectionItemInfo( 0, 1, - it.taskViewCount - it.indexOfChild(this@TaskView) - 1, + // We only care about TaskView's for the `CollectionInfo` that Talkback uses + // to read out. + it.taskViews.reversed().indexOf(this@TaskView), 1, - false + false, ) } } @@ -637,14 +775,15 @@ constructor( override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean { // TODO(b/343708271): Add support for multiple tasks per action. - if (action == R.id.action_close) { - recentsView?.dismissTask(this, true /*animateTaskView*/, true /*removeTask*/) - return true - } - taskContainers.forEach { - if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) { - return true + if (enableRefactorDigitalWellbeingToast() && it.taskContentView is TaskContentView) { + if (it.taskContentView.handleAccessibilityAction(action)) { + return true + } + } else { + if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) { + return true + } } TaskOverlayFactory.getEnabledShortcuts(this, it).forEach { shortcut -> @@ -658,83 +797,300 @@ constructor( return super.performAccessibilityAction(action, arguments) } + override fun onFinishInflate() { + super.onFinishInflate() + inflateViewStubs() + } + + protected open fun inflateViewStubs() { + findViewById(R.id.task_content_view) + ?.apply { + inflatedId = + if (enableRefactorTaskContentView()) R.id.task_content_view else R.id.snapshot + layoutResource = + when { + enableRefactorTaskContentView() -> R.layout.task_content_view + enableRefactorTaskThumbnail() -> R.layout.task_thumbnail + else -> R.layout.task_thumbnail_deprecated + } + } + ?.inflate() + + findViewById(R.id.icon) + ?.apply { + layoutResource = + if (enableOverviewIconMenu()) R.layout.icon_app_chip_view + else R.layout.icon_view + } + ?.inflate() + + if (!enableRefactorDigitalWellbeingToast()) { + findViewById(R.id.digital_wellbeing_toast) + ?.apply { layoutResource = R.layout.digital_wellbeing_toast } + ?.inflate() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (enableRefactorTaskThumbnail()) { + // TaskView binds the ViewModel during onBind, and unbinds it in onRecycle. So it + // should start listening here. + // TV Lifecycle: onBind -> onAttachedToWindow -> onDetachFromWindow -> onRecycle + coroutineJobs += + coroutineScope.launch(dispatcherProvider.main) { + viewModel!!.state.collectLatest(::updateTaskViewState) + } + } + } + + private fun updateTaskViewState(state: TaskTileUiState) { + sysUiStatusNavFlags = state.sysUiStatusNavFlags + + // Updating containers + val mapOfTasks = state.tasks.associateBy { it.taskId } + taskContainers.forEach { container -> + val taskId = container.task.key.id + val containerState = mapOfTasks[taskId] + val shouldHaveHeader = (type == TaskViewType.DESKTOP) && enableDesktopExplodedView() + val shouldShowAppTimer = + (type == TaskViewType.SINGLE || type == TaskViewType.GROUPED) + container.setState( + state = containerState, + hasHeader = shouldHaveHeader, + canShowAppTimer = shouldShowAppTimer, + clickCloseListener = + if (shouldHaveHeader) { + { + // Update the layout UI to remove this task from the layout grid, + // and remove the task from ActivityManager afterwards. + recentsView?.dismissTask( + taskId, + /* animate= */ true, + /* removeTask= */ true, + ) + } + } else { + null + }, + ) + updateThumbnailValidity(container) + val thumbnailPosition = + updateThumbnailMatrix( + container = container, + width = container.thumbnailView.width, + height = container.thumbnailView.height, + ) + container.setOverlayEnabled(state.taskOverlayEnabled, thumbnailPosition) + if (state.isCentralTask) { + this.container.actionsView.let { + it.updateDisabledFlags(DISABLED_ROTATED, thumbnailPosition.isRotated) + it.updateDisabledFlags( + DISABLED_NO_THUMBNAIL, + state.tasks.any { taskData -> + (taskData as? TaskData.Data)?.thumbnailData?.thumbnail == null + }, + ) + } + } + + if (enableOverviewIconMenu()) { + setIconState(container, containerState) + if ( + containerState is TaskData && + container.digitalWellBeingToast?.isDestroyed == false && + container.task.titleDescription != null + ) { + container.digitalWellBeingToast.initialize() + } + } + } + } + + private fun updateThumbnailValidity(container: TaskContainer) { + container.isThumbnailValid = + viewModel!!.isThumbnailValid( + thumbnail = container.thumbnailData, + width = container.thumbnailView.width, + height = container.thumbnailView.height, + ) + applyThumbnailSplashAlpha() + } + + /** + * Updates the thumbnail's transformation matrix and rotation state within a TaskContainer. + * + * This function is called to reposition the thumbnail in the following scenarios: + * - When the TTV's size changes (onSizeChanged), and it's displaying a SnapshotSplash. + * - When drawing a snapshot (drawSnapshot). + * + * @param container The TaskContainer holding the thumbnail to be updated. + * @param width The desired width of the thumbnail's container. + * @param height The desired height of the thumbnail's container. + */ + private fun updateThumbnailMatrix( + container: TaskContainer, + width: Int, + height: Int, + ): ThumbnailPosition { + val thumbnailPosition = + viewModel!!.getThumbnailPosition( + container.thumbnailData, + width, + height, + isLayoutRtl, + ) + container.updateThumbnailMatrix(thumbnailPosition.matrix) + return thumbnailPosition + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + cancelJobs() + } + + fun cancelJobs() { + if (enableRefactorTaskThumbnail()) { + // The jobs are being cancelled in the background thread. So we make a copy of the + // list to prevent cleaning a new job that might be added to this list during + // onAttach or another moment in the lifecycle. + val coroutineJobsToCancel = coroutineJobs.toList() + coroutineJobs.clear() + if (coroutineJobsToCancel.isEmpty()) return + + if (enableCoroutineThreadingImprovements()) { + // TODO(b/391842220): This should ideally be handled in the completion block of the + // jobs above to be cancelled. + taskContainers.forEach { + it.setState( + state = null, + hasHeader = false, + canShowAppTimer = false, + clickCloseListener = null, + ) + // Do not set icon to null if we are actively in split selection. The task + // appears to have been offloaded as we remove it during split, but we still + // need the icon to show over the split task. + if (enableOverviewIconMenu() && recentsView?.isSplitSelectionActive == false) { + setIconState(it, null) + } + } + } + + coroutineScope.launch(dispatcherProvider.lightweightBackground) { + coroutineJobsToCancel.forEach { + it.cancel("TaskView detaching from window") + } + } + } + } + /** Updates this task view to the given {@param task}. */ open fun bind( - task: Task, + singleTask: SingleTask, orientedState: RecentsOrientedState, - taskOverlayFactory: TaskOverlayFactory + taskOverlayFactory: TaskOverlayFactory, ) { + this.groupTask = singleTask cancelPendingLoadTasks() + this.orientedState = orientedState // Needed for dependencies taskContainers = listOf( createTaskContainer( - task, + singleTask.task, + R.id.task_content_view, R.id.snapshot, R.id.icon, R.id.show_windows, + R.id.digital_wellbeing_toast, STAGE_POSITION_UNDEFINED, - taskOverlayFactory + taskOverlayFactory, ) ) - setOrientationState(orientedState) + onBind(orientedState) } + protected open fun onBind(orientedState: RecentsOrientedState) { + if (enableRefactorTaskThumbnail()) { + Log.d(TAG, "onBind $context ${orientedState.containerInterface}") + viewModel!!.bind(*taskIds) + } + + taskContainers.forEach { container -> + container.bind() + if (enableRefactorTaskContentView()) { + (container.taskContentView as TaskContentView).cornerRadius = + thumbnailFullscreenParams.currentCornerRadius + container.taskContentView.doOnSizeChange { width, height -> + updateThumbnailValidity(container) + val thumbnailPosition = updateThumbnailMatrix(container, width, height) + container.refreshOverlay(thumbnailPosition) + } + } else if (enableRefactorTaskThumbnail()) { + container.thumbnailView.cornerRadius = + thumbnailFullscreenParams.currentCornerRadius + container.thumbnailView.doOnSizeChange { width, height -> + updateThumbnailValidity(container) + val thumbnailPosition = updateThumbnailMatrix(container, width, height) + container.refreshOverlay(thumbnailPosition) + } + } + } + setOrientationState(orientedState) + } + + private fun applyThumbnailSplashAlpha() { + val alpha = getSplashAlphaProgress() + taskContainers.forEach { it.updateThumbnailSplashProgress(alpha) } + } + + private fun getSplashAlphaProgress(): Float = + when { + !enableRefactorTaskThumbnail() -> taskThumbnailSplashAlpha + splitSplashAlpha > 0f -> splitSplashAlpha + shouldShowSplash() -> taskThumbnailSplashAlpha + else -> 0f + } + + internal fun shouldShowSplash(): Boolean = taskContainers.any { !it.isThumbnailValid } + protected fun createTaskContainer( task: Task, + @IdRes taskContentViewId: Int, @IdRes thumbnailViewId: Int, @IdRes iconViewId: Int, @IdRes showWindowViewId: Int, + @IdRes digitalWellbeingBannerId: Int, @StagePosition stagePosition: Int, - taskOverlayFactory: TaskOverlayFactory + taskOverlayFactory: TaskOverlayFactory, ): TaskContainer { - val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!! - val thumbnailView: TaskThumbnailView? - if (enableRefactorTaskThumbnail()) { - val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated) - thumbnailView = - TaskThumbnailView(context).apply { - layoutParams = thumbnailViewDeprecated.layoutParams - addView(this, indexOfSnapshotView) + val iconView = findViewById(iconViewId) as TaskViewIcon + val taskContentView = + if (enableRefactorTaskContentView()) findViewById(taskContentViewId) + else findViewById(thumbnailViewId) + val snapshotView = + if (enableRefactorTaskContentView()) taskContentView.findViewById(thumbnailViewId) + else taskContentView + + val digitalWellBeingToast: DigitalWellBeingToast? = + if (enableRefactorDigitalWellbeingToast()) { + null + } else { + findViewById(digitalWellbeingBannerId)!! } - thumbnailViewDeprecated.visibility = GONE - } else { - thumbnailView = null - } - val iconView = getOrInflateIconView(iconViewId) - return TaskContainer( + return TaskContainer( + this, task, - thumbnailView, - thumbnailViewDeprecated, + taskContentView, + snapshotView, iconView, TransformingTouchDelegate(iconView.asView()), stagePosition, - DigitalWellBeingToast(container, this), + digitalWellBeingToast, findViewById(showWindowViewId)!!, - taskOverlayFactory + taskOverlayFactory, ) - .apply { - if (enableRefactorTaskThumbnail()) { - thumbnailViewDeprecated.setTaskOverlay(overlay) - bindThumbnailView() - } else { - thumbnailViewDeprecated.bind(task, overlay) - } - } - } - - protected fun getOrInflateIconView(@IdRes iconViewId: Int): TaskViewIcon { - val iconView = findViewById(iconViewId)!! - return iconView as? TaskViewIcon - ?: (iconView as ViewStub) - .apply { - layoutResource = - if (enableOverviewIconMenu()) R.layout.icon_app_chip_view - else R.layout.icon_view - } - .inflate() as TaskViewIcon - } - - protected fun isTaskContainersInitialized() = this::taskContainers.isInitialized + } fun containsMultipleTasks() = taskContainers.size > 1 @@ -748,15 +1104,15 @@ constructor( fun containsTaskId(taskId: Int) = getTaskContainerById(taskId) != null open fun setOrientationState(orientationState: RecentsOrientedState) { - this.orientedState = orientationState - taskContainers.forEach { it.iconView.setIconOrientation(orientationState, isGridTask) } - setThumbnailOrientation(orientationState) - } + this.orientedState = orientationState + taskContainers.forEach { it.iconView.setIconOrientation(orientationState, isGridTask) } + setThumbnailOrientation(orientationState) + } protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) { taskContainers.forEach { it.overlay.updateOrientationState(orientationState) - it.digitalWellBeingToast?.initialize(it.task) + it.digitalWellBeingToast?.initialize() } } @@ -764,24 +1120,21 @@ constructor( * Updates TaskView scaling and translation required to support variable width if enabled, while * ensuring TaskView fits into screen in fullscreen. */ - fun updateTaskSize( - lastComputedTaskSize: Rect, - lastComputedGridTaskSize: Rect, - lastComputedCarouselTaskSize: Rect - ) { - val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx + open fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) { + val thumbnailPadding = container.deviceProfile.overviewProfile.taskThumbnailTopMarginPx val taskWidth = lastComputedTaskSize.width() val taskHeight = lastComputedTaskSize.height() val nonGridScale: Float val boxTranslationY: Float val expectedWidth: Int val expectedHeight: Int - if (container.deviceProfile.isTablet) { + if (container.deviceProfile.getDeviceProperties().isTablet) { val boxWidth: Int val boxHeight: Int - if (isFocusedTask) { - // Task will be focused and should use focused task size. Use focusTaskRatio - // that is associated with the original orientation of the focused task. + + // Focused task and Desktop tasks should use focusTaskRatio that is associated + // with the original orientation of the focused task. + if (isLargeTile) { boxWidth = taskWidth boxHeight = taskHeight } else { @@ -795,22 +1148,15 @@ constructor( expectedHeight = boxHeight + thumbnailPadding // Scale to to fit task Rect. - nonGridScale = - if (enableGridOnlyOverview()) { - lastComputedCarouselTaskSize.width() / taskWidth.toFloat() - } else { - taskWidth / boxWidth.toFloat() - } + nonGridScale = taskWidth / boxWidth.toFloat() // Align to top of task Rect. boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f } else { nonGridScale = 1f boxTranslationY = 0f - expectedWidth = if (enableOverviewIconMenu()) taskWidth else LayoutParams.MATCH_PARENT - expectedHeight = - if (enableOverviewIconMenu()) taskHeight + thumbnailPadding - else LayoutParams.MATCH_PARENT + expectedWidth = taskWidth + expectedHeight = taskHeight + thumbnailPadding } this.nonGridScale = nonGridScale this.boxTranslationY = boxTranslationY @@ -824,9 +1170,10 @@ constructor( protected open fun updateThumbnailSize() { // TODO(b/271468547), we should default to setting translations only on the snapshot instead // of a hybrid of both margins and translations - taskContainers[0].snapshotView.updateLayoutParams { - topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx + firstTaskContainer?.taskContentView?.updateLayoutParams { + topMargin = container.deviceProfile.overviewProfile.taskThumbnailTopMarginPx } + taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() } } /** Returns the thumbnail's bounds, optionally relative to the screen. */ @@ -837,11 +1184,11 @@ constructor( val thumbnailBounds = Rect() if (relativeToDragLayer) { container.dragLayer.getDescendantRectRelativeToSelf( - it.snapshotView, - thumbnailBounds + it.taskContentView, + thumbnailBounds, ) } else { - thumbnailBounds.set(it.snapshotView) + thumbnailBounds.set(it.taskContentView) } bounds.union(thumbnailBounds) } @@ -870,7 +1217,8 @@ constructor( taskContainers.forEach { if (visible) { recentsModel.thumbnailCache - .updateThumbnailInBackground(it.task) { thumbnailData -> + .getThumbnailInBackground(it.task) { thumbnailData -> + it.task.thumbnail = thumbnailData it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData) } ?.also { request -> pendingThumbnailLoadRequests.add(request) } @@ -882,28 +1230,27 @@ constructor( } } } - if (needsUpdate(changes, FLAG_UPDATE_ICON)) { + if ( + needsUpdate(changes, FLAG_UPDATE_ICON) && + !(enableOverviewIconMenu() && enableRefactorTaskThumbnail()) + ) { taskContainers.forEach { if (visible) { recentsModel.iconCache - .updateIconInBackground(it.task) { task -> - setIcon(it.iconView, task.icon) - if (enableOverviewIconMenu()) { - setText(it.iconView, task.title) - } - it.digitalWellBeingToast?.initialize(task) + .getIconInBackground(it.task) { icon, contentDescription, title -> + it.task.icon = icon + it.task.titleDescription = contentDescription + it.task.title = title + onIconLoaded(it) } ?.also { request -> pendingIconLoadRequests.add(request) } } else { - setIcon(it.iconView, null) - if (enableOverviewIconMenu()) { - setText(it.iconView, null) - } + onIconUnloaded(it) } } } if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { - currentFullscreenParams.updateCornerRadius(context) + thumbnailFullscreenParams.updateCornerRadius(context) } } @@ -911,40 +1258,68 @@ constructor( (dataChange and flag) == flag protected open fun cancelPendingLoadTasks() { - pendingThumbnailLoadRequests.forEach { it.cancel() } - pendingThumbnailLoadRequests.clear() - pendingIconLoadRequests.forEach { it.cancel() } - pendingIconLoadRequests.clear() + pendingThumbnailLoadRequests.forEach { it.cancel() } + pendingThumbnailLoadRequests.clear() + pendingIconLoadRequests.forEach { it.cancel() } + pendingIconLoadRequests.clear() + } + + protected open fun setIconState(container: TaskContainer, state: TaskData?) { + if (enableOverviewIconMenu()) { + if (state is TaskData.Data) { + setIcon(container.iconView, state.icon) + container.iconView.setText(state.title) + } else { + setIcon(container.iconView, null) + container.iconView.setText(null) + } + } + } + + protected open fun onIconLoaded(taskContainer: TaskContainer) { + setIcon(taskContainer.iconView, taskContainer.task.icon) + if (enableOverviewIconMenu()) { + taskContainer.iconView.setText(taskContainer.task.title) + } + taskContainer.digitalWellBeingToast?.initialize() + } + + protected open fun onIconUnloaded(taskContainer: TaskContainer) { + setIcon(taskContainer.iconView, null) + if (enableOverviewIconMenu()) { + taskContainer.iconView.setText(null) + } } protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) { with(iconView) { if (icon != null) { setDrawable(icon) - setOnClickListener { + asView().setOnClickListener { if (!confirmSecondSplitSelectApp()) { showTaskMenu(this) } } - setOnLongClickListener { + asView().setOnLongClickListener { requestDisallowInterceptTouchEvent(true) showTaskMenu(this) } } else { setDrawable(null) - setOnClickListener(null) - setOnLongClickListener(null) + asView().setOnClickListener(null) + asView().setOnLongClickListener(null) } } } - protected fun setText(iconView: TaskViewIcon, text: CharSequence?) { - iconView.setText(text) - } - - open fun refreshThumbnails(thumbnailDatas: HashMap?) { + @JvmOverloads + open fun setShouldShowScreenshot( + shouldShowScreenshot: Boolean, + thumbnailDatas: Map? = null, + ) { + if (this.shouldShowScreenshot == shouldShowScreenshot) return + this.shouldShowScreenshot = shouldShowScreenshot if (enableRefactorTaskThumbnail()) { - // TODO(b/342560598) add thumbnail logic return } @@ -964,7 +1339,7 @@ constructor( return } val callbackList = - launchTasks()?.apply { + launchWithAnimation()?.apply { add { Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted") } @@ -972,175 +1347,70 @@ constructor( Log.d("b/310064698", "${taskIds.contentToString()} - onClick - callbackList: $callbackList") container.statsLogManager .logger() - .withItemInfo(firstItemInfo) + .withItemInfo(itemInfo) .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP) } - /** - * Starts the task associated with this view and animates the startup. - * - * @return CompletionStage to indicate the animation completion or null if the launch failed. - */ - open fun launchTaskAnimated(): RunnableList? { - TestLogging.recordEvent( - TestProtocol.SEQUENCE_MAIN, - "startActivityFromRecentsAsync", - taskIds.contentToString() - ) - val opts = - container.getActivityLaunchOptions(this, null).apply { - options.launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY - } - if ( - ActivityManagerWrapper.getInstance() - .startActivityFromRecents(taskContainers[0].task.key, opts.options) - ) { - Log.d( - TAG, - "launchTaskAnimated - startActivityFromRecents: ${taskIds.contentToString()}" - ) - ActiveGestureLog.INSTANCE.trackEvent( - ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED - ) - val recentsView = recentsView ?: return null - if (recentsView.runningTaskViewId != -1) { - recentsView.onTaskLaunchedInLiveTileMode() - - // Return a fresh callback in the live tile case, so that it's not accidentally - // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner. - return RunnableList().also { recentsView.addSideTaskLaunchCallback(it) } - } - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - // If the recents transition is running (ie. in live tile mode), then the start - // of a new task will merge into the existing transition and it currently will - // not be run independently, so we need to rely on the onTaskAppeared() call - // for the new task to trigger the side launch callback to flush this runnable - // list (which is usually flushed when the app launch animation finishes) - recentsView.addSideTaskLaunchCallback(opts.onEndCallback) - } - return opts.onEndCallback - } else { - notifyTaskLaunchFailed() - return null - } - } - - /** Starts the task associated with this view without any animation */ - fun launchTask(callback: (launched: Boolean) -> Unit) { - launchTask(callback, isQuickSwitch = false) - } - - /** Starts the task associated with this view without any animation */ - open fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { - TestLogging.recordEvent( - TestProtocol.SEQUENCE_MAIN, - "startActivityFromRecentsAsync", - taskIds.contentToString() - ) - val firstContainer = taskContainers[0] - val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext) - if (isQuickSwitch) { - // We only listen for failures to launch in quickswitch because the during this - // gesture launcher is in the background state, vs other launches which are in - // the actual overview state - failureListener.register(container, firstContainer.task.key.id) { - notifyTaskLaunchFailed() - recentsView?.let { - // Disable animations for now, as it is an edge case and the app usually - // covers launcher and also any state transition animation also gets - // clobbered by QuickstepTransitionManager.createWallpaperOpenAnimations - // when launcher shows again - it.startHome(false /* animated */) - // LauncherTaskbarUIController depends on the launcher state when - // checking whether to handle resume, but that can come in before - // startHome() changes the state, so force-refresh here to ensure the - // taskbar is updated - it.mSizeStrategy.taskbarController?.refreshResumedState() - } - } - } - // Indicate success once the system has indicated that the transition has started - val opts = - ActivityOptions.makeCustomTaskAnimation( - context, - 0, - 0, - Executors.MAIN_EXECUTOR.handler, - { callback(true) } - ) { - failureListener.onTransitionFinished() - } - .apply { - launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY - if (isQuickSwitch) { - setFreezeRecentTasksReordering() - } - // TODO(b/334826842) add splash functionality to new TTV - if (!enableRefactorTaskThumbnail()) { - disableStartingWindow = - firstContainer.thumbnailViewDeprecated.shouldShowSplashView() - } - } - Executors.UI_HELPER_EXECUTOR.execute { - if ( - !ActivityManagerWrapper.getInstance() - .startActivityFromRecents(firstContainer.task.key, if (Utilities.ATLEAST_Q) opts else null) - ) { - // If the call to start activity failed, then post the result immediately, - // otherwise, wait for the animation start callback from the activity options - // above - Executors.MAIN_EXECUTOR.post { - notifyTaskLaunchFailed() - callback(false) - } - } - Log.d(TAG, "launchTask - startActivityFromRecents: ${taskIds.contentToString()}") - } - } - /** Launch of the current task (both live and inactive tasks) with an animation. */ - fun launchTasks(): RunnableList? { - val recentsView = recentsView ?: return null - val remoteTargetHandles = recentsView.mRemoteTargetHandles - if (!isRunningTask || remoteTargetHandles == null) { - return launchTaskAnimated() + fun launchWithAnimation(): RunnableList? { + return if (isRunningTask && recentsView?.remoteTargetHandles != null) { + launchAsLiveTile(recentsView?.remoteTargetHandles!!) + } else { + launchAsStaticTile() } + } + + private fun launchAsLiveTile(remoteTargetHandles: Array): RunnableList? { + val recentsView = recentsView ?: return null if (!isClickableAsLiveTile) { - Log.e(TAG, "TaskView is not clickable as a live tile; returning to home.") + Log.e( + TAG, + "launchAsLiveTile - TaskView is not clickable as a live tile; returning to home: ${taskIds.contentToString()}", + ) return null } isClickableAsLiveTile = false val targets = - if (remoteTargetHandles.size == 1) { - remoteTargetHandles[0].transformParams.targetSet + if (remoteTargetHandles.isNotEmpty()) { + if (remoteTargetHandles.size == 1) { + remoteTargetHandles[0].transformParams.targetSet + } else { + val apps = + remoteTargetHandles.flatMap { + it.transformParams.targetSet.apps.asIterable() + } + val wallpapers = + remoteTargetHandles.flatMap { + it.transformParams.targetSet.wallpapers.asIterable() + } + RemoteAnimationTargets( + apps.toTypedArray(), + wallpapers.toTypedArray(), + remoteTargetHandles[0].transformParams.targetSet.nonApps, + remoteTargetHandles[0].transformParams.targetSet.targetMode, + ) + } } else { - val apps = - remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() } - val wallpapers = - remoteTargetHandles.flatMap { - it.transformParams.targetSet.wallpapers.asIterable() - } - RemoteAnimationTargets( - apps.toTypedArray(), - wallpapers.toTypedArray(), - remoteTargetHandles[0].transformParams.targetSet.nonApps, - remoteTargetHandles[0].transformParams.targetSet.targetMode - ) + null } if (targets == null) { // If the recents animation is cancelled somehow between the parent if block and // here, try to launch the task as a non live tile task. - val runnableList = launchTaskAnimated() + val runnableList = launchAsStaticTile() if (runnableList == null) { Log.e( TAG, - "Recents animation cancelled and cannot launch task as non-live tile" + - "; returning to home" + "launchAsLiveTile - Recents animation cancelled and cannot launch task as non-live tile; returning to home: ${taskIds.contentToString()}", ) } isClickableAsLiveTile = true return runnableList } + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "composeRecentsLaunchAnimator", + taskIds.contentToString(), + ) val runnableList = RunnableList() with(AnimatorSet()) { TaskViewUtils.composeRecentsLaunchAnimator( @@ -1149,16 +1419,17 @@ constructor( targets.apps, targets.wallpapers, targets.nonApps, - true /* launcherClosing */, + true, /* launcherClosing */ recentsView.stateManager, recentsView, - recentsView.depthController + recentsView.depthController, + /* transitionInfo= */ null, ) addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animator: Animator) { if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) { - launchTaskAnimated() + launchAsStaticTile() } isClickableAsLiveTile = true runEndCallback() @@ -1175,13 +1446,144 @@ constructor( ) start() } - Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: ${taskIds.contentToString()}") + Log.d(TAG, "launchAsLiveTile - composeRecentsLaunchAnimator: ${taskIds.contentToString()}") recentsView.onTaskLaunchedInLiveTileMode() return runnableList } - private fun notifyTaskLaunchFailed() { - val sb = StringBuilder("Failed to launch task \n") + /** + * Starts the task associated with this view and animates the startup. + * + * @return CompletionStage to indicate the animation completion or null if the launch failed. + */ + open fun launchAsStaticTile(): RunnableList? { + val firstTaskContainer = firstTaskContainer ?: return null + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "startActivityFromRecentsAsync", + taskIds.contentToString(), + ) + val opts = + container.getActivityLaunchOptions(this, null).apply { + options.launchDisplayId = displayId + } + if ( + ActivityManagerWrapper.getInstance() + .startActivityFromRecents(firstTaskContainer.task.key, opts.options) + ) { + Log.d( + TAG, + "launchAsStaticTile - startActivityFromRecents: ${taskIds.contentToString()}", + ) + ActiveGestureLog.INSTANCE.trackEvent( + ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED + ) + val recentsView = recentsView ?: return null + if ( + recentsView.runningTaskViewId != -1 && + recentsView.mRecentsAnimationController != null + ) { + recentsView.onTaskLaunchedInLiveTileMode() + + // Return a fresh callback in the live tile case, so that it's not accidentally + // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner. + return RunnableList().also { recentsView.addSideTaskLaunchCallback(it) } + } + // If the recents transition is running (ie. in live tile mode), then the start + // of a new task will merge into the existing transition and it currently will + // not be run independently, so we need to rely on the onTaskAppeared() call + // for the new task to trigger the side launch callback to flush this runnable + // list (which is usually flushed when the app launch animation finishes) + recentsView.addSideTaskLaunchCallback(opts.onEndCallback) + return opts.onEndCallback + } else { + notifyTaskLaunchFailed("launchAsStaticTile") + return null + } + } + + /** Starts the task associated with this view without any animation */ + @JvmOverloads + open fun launchWithoutAnimation( + isQuickSwitch: Boolean = false, + callback: (launched: Boolean) -> Unit, + ) { + val callbackWithLogging = { launchSuccess: Boolean -> + Log.d(TAG, "launchWithoutAnimation - callback: launchSuccess: $launchSuccess") + callback(launchSuccess) + } + val firstTaskContainer = firstTaskContainer ?: return + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "startActivityFromRecentsAsync", + taskIds.contentToString(), + ) + val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext) + if (isQuickSwitch) { + // We only listen for failures to launch in quickswitch because the during this + // gesture launcher is in the background state, vs other launches which are in + // the actual overview state + failureListener.register(container, firstTaskContainer.task.key.id) { + notifyTaskLaunchFailed("launchWithoutAnimation") + recentsView?.let { + // Disable animations for now, as it is an edge case and the app usually + // covers launcher and also any state transition animation also gets + // clobbered by QuickstepTransitionManager.createWallpaperOpenAnimations + // when launcher shows again + it.startHome(false /* animated */) + // LauncherTaskbarUIController depends on the launcher state when + // checking whether to handle resume, but that can come in before + // startHome() changes the state, so force-refresh here to ensure the + // taskbar is updated + it.mContainerInterface.taskbarController?.refreshResumedState() + } + } + } + // Indicate success once the system has indicated that the transition has started + val opts = + ActivityOptions.makeCustomTaskAnimation( + context, + 0, + 0, + Executors.MAIN_EXECUTOR.handler, + { callbackWithLogging(true) }, + ) { + Log.d(TAG, "launchWithoutAnimation: launch animation finished") + failureListener.onTransitionFinished() + } + .apply { + launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY + if (isQuickSwitch) { + setFreezeRecentTasksReordering() + } + // TODO(b/331754864): Update this to use TV.shouldShowSplash + disableStartingWindow = firstTaskContainer.shouldShowSplashView + } + Executors.UI_HELPER_EXECUTOR.execute { + Log.d( + TAG, + "launchWithoutAnimation(isQuickSwitch: $isQuickSwitch) - " + + "startActivityFromRecents: ${taskIds.contentToString()}", + ) + if ( + !ActivityManagerWrapper.getInstance() + .startActivityFromRecents(firstTaskContainer.task.key, if (Utilities.ATLEAST_Q) opts else null) + ) { + Log.d(TAG, "launchWithoutAnimation - task launch failed") + // If the call to start activity failed, then post the result immediately, + // otherwise, wait for the animation start callback from the activity options + // above + Executors.MAIN_EXECUTOR.post { + notifyTaskLaunchFailed("launchTask") + callbackWithLogging(false) + } + } + } + } + + private fun notifyTaskLaunchFailed(launchMethod: String) { + val sb = + StringBuilder("$launchMethod - Failed to launch task: ${taskIds.contentToString()}\n") taskContainers.forEach { sb.append("(task=${it.task.key.baseIntent} userId=${it.task.key.userId})\n") } @@ -1189,14 +1591,6 @@ constructor( Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show() } - fun initiateSplitSelect(splitPositionOption: SplitPositionOption) { - recentsView?.initiateSplitSelect( - this, - splitPositionOption.stagePosition, - SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition) - ) - } - /** * Returns `true` if user is already in split select mode and this tap was to choose the second * app. `false` otherwise @@ -1212,11 +1606,11 @@ constructor( this, container.task, container.iconView.drawable, - container.thumbnailViewDeprecated, - container.thumbnailViewDeprecated.thumbnail, /* intent */ - null, /* user */ - null, - container.itemInfo + container.snapshotView, + container.thumbnail, + /* intent */ null, + /* user */ null, + container.itemInfo, ) } @@ -1233,7 +1627,9 @@ constructor( // Don't show menu when selecting second split screen app return true } - if (!container.deviceProfile.isTablet && !recentsView.isClearAllHidden) { + if ( + !container.deviceProfile.getDeviceProperties().isTablet && !recentsView.isClearAllHidden + ) { recentsView.snapToPage(recentsView.indexOfChild(this)) return false } @@ -1245,18 +1641,35 @@ constructor( return showTaskMenuWithContainer(menuContainer) } + private fun closeTaskMenu(): Boolean { + val floatingView: AbstractFloatingView? = + AbstractFloatingView.getTopOpenViewWithType( + container, + AbstractFloatingView.TYPE_TASK_MENU, + ) + if (floatingView?.isOpen == true) { + floatingView.close(true) + return true + } else { + return false + } + } + private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean { val recentsView = recentsView ?: return false + // Disable hover on all TaskView's whilst menu is showing. + recentsView.setTaskBorderEnabled(false) return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) { - menuContainer.iconView.revealAnim(/* isRevealing= */ true) - TaskMenuView.showForTask(menuContainer) { - menuContainer.iconView.revealAnim(/* isRevealing= */ false) + if (menuContainer.iconView.status == AppChipStatus.Expanded) { + closeTaskMenu() + } else { + TaskMenuView.showForTask(menuContainer) { recentsView.setTaskBorderEnabled(true) } } - } else if (container.deviceProfile.isTablet) { + } else if (container.deviceProfile.getDeviceProperties().isTablet) { val alignedOptionIndex = if ( recentsView.isOnGridBottomRow(menuContainer.taskView) && - container.deviceProfile.isLandscape + container.deviceProfile.deviceProperties.isLandscape ) { if (enableGridOnlyOverview()) { // With no focused task, there is less available space below the tasks, so @@ -1270,9 +1683,11 @@ constructor( } else { 0 } - TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) + TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) { + recentsView.setTaskBorderEnabled(true) + } } else { - TaskMenuView.showForTask(menuContainer) + TaskMenuView.showForTask(menuContainer) { recentsView.setTaskBorderEnabled(true) } } } @@ -1295,10 +1710,10 @@ constructor( private fun computeAndSetIconTouchDelegate( view: TaskViewIcon, tempCenterCoordinates: FloatArray, - transformingTouchDelegate: TransformingTouchDelegate + transformingTouchDelegate: TransformingTouchDelegate, ) { - val viewHalfWidth = view.width / 2f - val viewHalfHeight = view.height / 2f + val viewHalfWidth = view.asView().width / 2f + val viewHalfHeight = view.asView().height / 2f Utilities.getDescendantCoordRelativeToAncestor( view.asView(), container.dragLayer, @@ -1306,13 +1721,13 @@ constructor( this[0] = viewHalfWidth this[1] = viewHalfHeight }, - false + false, ) transformingTouchDelegate.setBounds( (tempCenterCoordinates[0] - viewHalfWidth).toInt(), (tempCenterCoordinates[1] - viewHalfHeight).toInt(), (tempCenterCoordinates[0] + viewHalfWidth).toInt(), - (tempCenterCoordinates[1] + viewHalfHeight).toInt() + (tempCenterCoordinates[1] + viewHalfHeight).toInt(), ) } @@ -1322,7 +1737,7 @@ constructor( it.showWindowsView?.let { showWindowsView -> updateFilterCallback( showWindowsView, - getFilterUpdateCallback(it.task.key.packageName) + getFilterUpdateCallback(it.task.key.packageName), ) } } @@ -1355,23 +1770,27 @@ constructor( * Called to animate a smooth transition when going directly from an app into Overview (and vice * versa). Icons fade in, and DWB banners slide in with a "shift up" animation. */ - private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) { + private fun onSettledProgressUpdated(settledProgress: Float) { taskContainers.forEach { - it.iconView.setContentAlpha(focusTransitionProgress) - it.digitalWellBeingToast?.updateBannerOffset(1f - focusTransitionProgress) + it.iconView.setContentAlpha(settledProgress) + if (enableRefactorDigitalWellbeingToast() && it.taskContentView is TaskContentView) { + it.taskContentView.onParentAnimationProgress(settledProgress) + } else { + it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - settledProgress + } } } - fun animateIconScaleAndDimIntoView() { - iconAndDimAnimator?.cancel() - iconAndDimAnimator = - ObjectAnimator.ofFloat(focusTransitionScaleAndDim, MULTI_PROPERTY_VALUE, 0f, 1f).apply { - duration = SCALE_ICON_DURATION + fun startIconFadeInOnGestureComplete() { + iconFadeInOnGestureCompleteAnimator?.cancel() + iconFadeInOnGestureCompleteAnimator = + ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_GESTURE, 1f).apply { + duration = FADE_IN_ICON_DURATION interpolator = Interpolators.LINEAR addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - iconAndDimAnimator = null + iconFadeInOnGestureCompleteAnimator = null } } ) @@ -1379,20 +1798,21 @@ constructor( } } - fun setIconScaleAndDim(iconScale: Float) { - iconAndDimAnimator?.cancel() - focusTransitionScaleAndDim.value = iconScale + fun setIconVisibleForGesture(isVisible: Boolean) { + iconFadeInOnGestureCompleteAnimator?.cancel() + settledProgressGesture = if (isVisible) 1f else 0f } /** Set a color tint on the snapshot and supporting views. */ open fun setColorTint(amount: Float, tintColor: Int) { taskContainers.forEach { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334832108) Add scrim to new TTV + if (enableRefactorTaskThumbnail()) { + it.updateTintAmount(amount) + } else { it.thumbnailViewDeprecated.dimAlpha = amount } it.iconView.setIconColorTint(tintColor, amount) - it.digitalWellBeingToast?.setBannerColorTint(tintColor, amount) + it.digitalWellBeingToast?.setColorTint(tintColor, amount) } } @@ -1405,8 +1825,8 @@ constructor( open fun setThumbnailVisibility(visibility: Int, taskId: Int) { taskContainers.forEach { if (visibility == VISIBLE || it.task.key.id == taskId) { - it.snapshotView.visibility = visibility - it.digitalWellBeingToast?.setBannerVisibility(visibility) + it.taskContentView.visibility = visibility + it.digitalWellBeingToast?.visibility = visibility it.showWindowsView?.visibility = visibility it.overlay.setVisibility(visibility) } @@ -1414,16 +1834,13 @@ constructor( } open fun setOverlayEnabled(overlayEnabled: Boolean) { - // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView. - // and if it's still necessary we should support that in the new TTV class. if (!enableRefactorTaskThumbnail()) { - taskContainers.forEach { it.thumbnailViewDeprecated.setOverlayEnabled(overlayEnabled) } + taskContainers.forEach { it.setOverlayEnabled(overlayEnabled) } } } protected open fun refreshTaskThumbnailSplash() { if (!enableRefactorTaskThumbnail()) { - // TODO(b/334826842) add splash functionality to new TTV taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() } } } @@ -1431,27 +1848,15 @@ constructor( protected fun getScrollAdjustment(gridEnabled: Boolean) = if (gridEnabled) gridTranslationX else nonGridTranslationX - protected fun getOffsetAdjustment(gridEnabled: Boolean) = getScrollAdjustment(gridEnabled) + fun getOffsetAdjustment(gridEnabled: Boolean) = getScrollAdjustment(gridEnabled) fun getSizeAdjustment(fullscreenEnabled: Boolean) = if (fullscreenEnabled) nonGridScale else 1f private fun applyScale() { - val scale = persistentScale * dismissScale + val scale = persistentScale * dismissScale * Utilities.mapRange(modalness, 1f, modalScale) scaleX = scale scaleY = scale - if (enableRefactorTaskThumbnail()) { - taskViewData.scale.value = scale - } - updateSnapshotRadius() - } - - protected open fun applyThumbnailSplashAlpha() { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334826842) add splash functionality to new TTV - taskContainers.forEach { - it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha) - } - } + updateFullscreenParams() } private fun applyTranslationX() { @@ -1481,45 +1886,53 @@ constructor( protected open fun onFullscreenProgressChanged(fullscreenProgress: Float) { taskContainers.forEach { - it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE) + if (!enableOverviewIconMenu()) { + it.iconView.asView().visibility = if (fullscreenProgress < 1) VISIBLE else INVISIBLE + } it.overlay.setFullscreenProgress(fullscreenProgress) } - focusTransitionFullscreen.value = - FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress) - updateSnapshotRadius() + updateSettledProgressFullscreen(fullscreenProgress) + updateFullscreenParams() } - protected open fun updateSnapshotRadius() { - updateCurrentFullscreenParams() + protected fun updateSettledProgressFullscreen(fullscreenProgress: Float) { + settledProgressFullscreen = + SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress) + } + + protected open fun updateFullscreenParams() { + updateFullscreenParams(thumbnailFullscreenParams) taskContainers.forEach { - it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams()) - it.overlay.setFullscreenParams(getThumbnailFullscreenParams()) + if (enableRefactorTaskContentView()) { + (it.taskContentView as TaskContentView).cornerRadius = + thumbnailFullscreenParams.currentCornerRadius + } else if (enableRefactorTaskThumbnail()) { + it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius + } else { + it.thumbnailViewDeprecated.setFullscreenParams(thumbnailFullscreenParams) + } + it.overlay.setFullscreenParams(thumbnailFullscreenParams) } } - protected open fun updateCurrentFullscreenParams() { - updateFullscreenParams(currentFullscreenParams) - } - protected fun updateFullscreenParams(fullscreenParams: FullscreenDrawParams) { recentsView?.let { fullscreenParams.setProgress(fullscreenProgress, it.scaleX, scaleX) } } - protected open fun getThumbnailFullscreenParams(): FullscreenDrawParams = - currentFullscreenParams - private fun onModalnessUpdated(modalness: Float) { + isClickable = modalness == 0f taskContainers.forEach { - it.iconView.setModalAlpha(1 - modalness) - it.digitalWellBeingToast?.updateBannerOffset(modalness) + it.iconView.setModalAlpha(1f - modalness) + if (enableRefactorDigitalWellbeingToast() && it.taskContentView is TaskContentView) { + it.taskContentView.onParentAnimationProgress(1f - modalness) + } else { + it.digitalWellBeingToast?.bannerOffsetPercentage = modalness + } + } + if (enableGridOnlyOverview()) { + modalAlpha = if (isSelectedTask) 1f else (1f - modalness) + applyScale() } - } - - /** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */ - fun notifyIsRunningTaskUpdated() { - // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM - // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView - taskContainers.forEach { it.bindThumbnailView() } } fun resetPersistentViewTransforms() { @@ -1527,19 +1940,26 @@ constructor( gridTranslationX = 0f gridTranslationY = 0f boxTranslationY = 0f - nonGridPivotTranslationX = 0f + taskContainers.forEach { + it.snapshotView.translationX = 0f + it.snapshotView.translationY = 0f + } resetViewTransforms() } - open fun resetViewTransforms() { + fun resetViewTransforms() { + // Dismiss translation shouldn't reset if actively being dragged + if (!isBeingDraggedForDismissal) { + secondaryDismissTranslationProperty.setValue(this, 0f) + } + primaryDismissTranslationProperty.setValue(this, 0f) + // fullscreenTranslation and accumulatedTranslation should not be reset, as // resetViewTransforms is called during QuickSwitch scrolling. - dismissTranslationX = 0f taskOffsetTranslationX = 0f taskResistanceTranslationX = 0f splitSelectTranslationX = 0f gridEndTranslationX = 0f - dismissTranslationY = 0f taskOffsetTranslationY = 0f taskResistanceTranslationY = 0f if (recentsView?.isSplitSelectionActive != true) { @@ -1547,13 +1967,9 @@ constructor( } dismissScale = 1f translationZ = 0f - alpha = stableAlpha - setIconScaleAndDim(1f) + setIconVisibleForGesture(true) + settledProgressDismiss = 1f setColorTint(0f, 0) - if (!enableRefactorTaskThumbnail()) { - // TODO(b/335399428) add split select functionality to new TTV - taskContainers.forEach { it.thumbnailViewDeprecated.resetViewTransforms() } - } } private fun getGridTrans(endTranslation: Float) = @@ -1562,243 +1978,93 @@ constructor( private fun getNonGridTrans(endTranslation: Float) = endTranslation - getGridTrans(endTranslation) - /** We update and subsequently draw these in [fullscreenProgress]. */ - open class FullscreenDrawParams(context: Context) : SafeCloseable { - var cornerRadius = 0f - private var windowCornerRadius = 0f - var currentDrawnCornerRadius = 0f - - init { - updateCornerRadius(context) - } - - /** Recomputes the start and end corner radius for the given Context. */ - fun updateCornerRadius(context: Context) { - cornerRadius = computeTaskCornerRadius(context) - windowCornerRadius = computeWindowCornerRadius(context) - } - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - open fun computeTaskCornerRadius(context: Context): Float { - return TaskCornerRadius.get(context) - } - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - open fun computeWindowCornerRadius(context: Context): Float { - val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context) - - // The corner radius is fixed to match when Taskbar is persistent mode - return if ( - activityContext != null && - activityContext.deviceProfile?.isTaskbarPresent == true && - DisplayController.isTransientTaskbar(context) - ) { - context.resources - .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius) - .toFloat() - } else { - QuickStepContract.getWindowCornerRadius(context) - } - } - - /** Sets the progress in range [0, 1] */ - fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) { - currentDrawnCornerRadius = - Utilities.mapRange(fullscreenProgress, cornerRadius, windowCornerRadius) / - parentScale / - taskViewScale - } - - override fun close() {} + private fun MotionEvent.isWithinThumbnailBounds(): Boolean { + return thumbnailBounds.contains(x.toInt(), y.toInt()) } - /** Holder for all Task dependent information. */ - inner class TaskContainer( - val task: Task, - val thumbnailView: TaskThumbnailView?, - val thumbnailViewDeprecated: TaskThumbnailViewDeprecated, - val iconView: TaskViewIcon, - /** - * This technically can be a vanilla [android.view.TouchDelegate] class, however that class - * requires setting the touch bounds at construction, so we'd repeatedly be created many - * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows - * touch delegated bounds only to be updated. - */ - val iconTouchDelegate: TransformingTouchDelegate, - /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */ - @StagePosition val stagePosition: Int, - val digitalWellBeingToast: DigitalWellBeingToast?, - val showWindowsView: View?, - taskOverlayFactory: TaskOverlayFactory - ) { - val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this) - - val snapshotView: View - get() = thumbnailView ?: thumbnailViewDeprecated - - /** Builds proto for logging */ - val itemInfo: WorkspaceItemInfo - get() = - WorkspaceItemInfo().apply { - itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK - container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER - val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key) - user = componentKey.user - intent = Intent().setComponent(componentKey.componentName) - title = task.title - recentsView?.let { screenId = it.indexOfChild(this@TaskView) } - if (privateSpaceRestrictAccessibilityDrag()) { - if ( - UserCache.getInstance(context).getUserInfo(componentKey.user).isPrivate - ) { - runtimeStatusFlags = - runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE - } - } - } - - val taskView: TaskView - get() = this@TaskView - - fun destroy() { - digitalWellBeingToast?.destroy() - thumbnailView?.let { taskView.removeView(it) } - } - - // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM - // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView - fun bindThumbnailView() { - // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but - // this should be decided inside TaskThumbnailViewModel. - thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, isRunningTask)) + override fun addChildrenForAccessibility(outChildren: ArrayList) { + (if (isLayoutRtl) taskContainers.reversed() else taskContainers).forEach { + it.addChildForAccessibility(outChildren) } } companion object { private const val TAG = "TaskView" + + private enum class Alpha { + Stable, + Attach, + Split, + Modal, + } + + private enum class SettledProgress { + Fullscreen, + Gesture, + Dismiss, + } + const val FLAG_UPDATE_ICON = 1 const val FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON shl 1 const val FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL shl 1 const val FLAG_UPDATE_ALL = (FLAG_UPDATE_ICON or FLAG_UPDATE_THUMBNAIL or FLAG_UPDATE_CORNER_RADIUS) - const val FOCUS_TRANSITION_INDEX_FULLSCREEN = 0 - const val FOCUS_TRANSITION_INDEX_SCALE_AND_DIM = 1 - const val FOCUS_TRANSITION_INDEX_COUNT = 2 - /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */ const val MAX_PAGE_SCRIM_ALPHA = 0.4f - const val SCALE_ICON_DURATION: Long = 120 + const val FADE_IN_ICON_DURATION: Long = 120 private const val DIM_ANIM_DURATION: Long = 700 - private const val FOCUS_TRANSITION_THRESHOLD = - SCALE_ICON_DURATION.toFloat() / DIM_ANIM_DURATION - val FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR = + private const val SETTLE_TRANSITION_THRESHOLD = + FADE_IN_ICON_DURATION.toFloat() / DIM_ANIM_DURATION + val SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR = Interpolators.clampToProgress( Interpolators.FAST_OUT_SLOW_IN, - 1f - FOCUS_TRANSITION_THRESHOLD, - 1f + 1f - SETTLE_TRANSITION_THRESHOLD, + 1f, )!! + private val FADE_IN_ICON_INTERPOLATOR = Interpolators.LINEAR private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect()) - private val FOCUS_TRANSITION: FloatProperty = - object : FloatProperty("focusTransition") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.focusTransitionProgress = v - } + private val SETTLED_PROGRESS: FloatProperty = + KFloatProperty(TaskView::settledProgress) - override fun get(taskView: TaskView) = taskView.focusTransitionProgress - } + private val SETTLED_PROGRESS_GESTURE: FloatProperty = + KFloatProperty(TaskView::settledProgressGesture) + + private val SETTLED_PROGRESS_DISMISS: FloatProperty = + KFloatProperty(TaskView::settledProgressDismiss) private val SPLIT_SELECT_TRANSLATION_X: FloatProperty = - object : FloatProperty("splitSelectTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.splitSelectTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.splitSelectTranslationX - } + KFloatProperty(TaskView::splitSelectTranslationX) private val SPLIT_SELECT_TRANSLATION_Y: FloatProperty = - object : FloatProperty("splitSelectTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.splitSelectTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.splitSelectTranslationY - } + KFloatProperty(TaskView::splitSelectTranslationY) private val DISMISS_TRANSLATION_X: FloatProperty = - object : FloatProperty("dismissTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.dismissTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.dismissTranslationX - } + KFloatProperty(TaskView::dismissTranslationX) private val DISMISS_TRANSLATION_Y: FloatProperty = - object : FloatProperty("dismissTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.dismissTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.dismissTranslationY - } + KFloatProperty(TaskView::dismissTranslationY) private val TASK_OFFSET_TRANSLATION_X: FloatProperty = - object : FloatProperty("taskOffsetTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskOffsetTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.taskOffsetTranslationX - } + KFloatProperty(TaskView::taskOffsetTranslationX) private val TASK_OFFSET_TRANSLATION_Y: FloatProperty = - object : FloatProperty("taskOffsetTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskOffsetTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.taskOffsetTranslationY - } + KFloatProperty(TaskView::taskOffsetTranslationY) private val TASK_RESISTANCE_TRANSLATION_X: FloatProperty = - object : FloatProperty("taskResistanceTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskResistanceTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.taskResistanceTranslationX - } + KFloatProperty(TaskView::taskResistanceTranslationX) private val TASK_RESISTANCE_TRANSLATION_Y: FloatProperty = - object : FloatProperty("taskResistanceTranslationY") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.taskResistanceTranslationY = v - } - - override fun get(taskView: TaskView) = taskView.taskResistanceTranslationY - } + KFloatProperty(TaskView::taskResistanceTranslationY) @JvmField val GRID_END_TRANSLATION_X: FloatProperty = - object : FloatProperty("gridEndTranslationX") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.gridEndTranslationX = v - } - - override fun get(taskView: TaskView) = taskView.gridEndTranslationX - } + KFloatProperty(TaskView::gridEndTranslationX) @JvmField - val DISMISS_SCALE: FloatProperty = - object : FloatProperty("dismissScale") { - override fun setValue(taskView: TaskView, v: Float) { - taskView.dismissScale = v - } + val DISMISS_SCALE: FloatProperty = KFloatProperty(TaskView::dismissScale) - override fun get(taskView: TaskView) = taskView.dismissScale - } + @JvmField val SPLIT_ALPHA: FloatProperty = KFloatProperty(TaskView::splitAlpha) } } diff --git a/quickstep/src/com/android/quickstep/views/TaskViewIcon.java b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java index 94739cbc90..04776e067d 100644 --- a/quickstep/src/com/android/quickstep/views/TaskViewIcon.java +++ b/quickstep/src/com/android/quickstep/views/TaskViewIcon.java @@ -27,16 +27,6 @@ import com.android.quickstep.util.RecentsOrientedState; */ public interface TaskViewIcon { - /** - * Returns the width of this icon view. - */ - int getWidth(); - - /** - * Returns the height of this icon view. - */ - int getHeight(); - /** * Sets the opacity of the view. */ @@ -47,6 +37,11 @@ public interface TaskViewIcon { */ void setModalAlpha(float alpha); + /** + * Sets the opacity of the view for flex split state. + */ + void setFlexSplitAlpha(float alpha); + /** * Returns this icon view's drawable. */ @@ -57,26 +52,6 @@ public interface TaskViewIcon { */ void setDrawable(@Nullable Drawable icon); - /** - * Register a callback to be invoked when this view is clicked. - */ - void setOnClickListener(@Nullable View.OnClickListener l); - - /** - * Register a callback to be invoked when this view is clicked and held. - */ - void setOnLongClickListener(@Nullable View.OnLongClickListener l); - - /** - * Returns the LayoutParams associated with this view. - */ - ViewGroup.LayoutParams getLayoutParams(); - - /** - * Sets the layout parameters associated with this view. - */ - void setLayoutParams(ViewGroup.LayoutParams params); - /** * Sets the degrees that the view is rotated around the pivot point. */ @@ -92,11 +67,6 @@ public interface TaskViewIcon { */ void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask); - /** - * Sets the visibility state of this view. - */ - void setVisibility(int visibility); - /** * Sets the tint color of the icon, useful for scrimming or dimming. * @@ -105,11 +75,6 @@ public interface TaskViewIcon { */ void setIconColorTint(int color, float amount); - /** - * Gets the opacity of the view. - */ - float getAlpha(); - /** * Returns the width of this icon view's drawable. */ @@ -120,16 +85,6 @@ public interface TaskViewIcon { */ int getDrawableHeight(); - /** - * Directly calls any attached OnClickListener. - */ - boolean callOnClick(); - - /** - * Calls this view's OnLongClickListener. - */ - boolean performLongClick(); - /** * Sets the text for this icon view if any text view is associated. */ diff --git a/quickstep/src/com/android/quickstep/views/TaskViewType.kt b/quickstep/src/com/android/quickstep/views/TaskViewType.kt new file mode 100644 index 0000000000..b2a32a96c8 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskViewType.kt @@ -0,0 +1,24 @@ +/* + * 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.quickstep.views + +/** Type of the [TaskView] */ +enum class TaskViewType { + SINGLE, + GROUPED, + DESKTOP +} diff --git a/quickstep/src_protolog/com/android/launcher3/util/OverviewCommandHelperProtoLogProxy.java b/quickstep/src_protolog/com/android/launcher3/util/OverviewCommandHelperProtoLogProxy.java new file mode 100644 index 0000000000..a3941b3e3c --- /dev/null +++ b/quickstep/src_protolog/com/android/launcher3/util/OverviewCommandHelperProtoLogProxy.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import static com.android.quickstep.util.QuickstepProtoLogGroup.OVERVIEW_COMMAND_HELPER; +import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.protolog.ProtoLog; + +/** + * Proxy class used for OverviewCommandHelper ProtoLog support. (e.g. for 3 button nav) + */ +public class OverviewCommandHelperProtoLogProxy { + + public static void logCommandQueueFull(@NonNull Object type, @NonNull Object commandQueue) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "command not added: %s - queue is full (%s).", + type, + commandQueue); + } + + public static void logCommandAdded(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "command added: %s", command); + } + + public static void logCommandExecuted(@NonNull Object command, int queueSize) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "execute: %s - queue size: %d", + command, + queueSize); + } + + public static void logCommandNotExecuted(@NonNull Object command, int queueSize) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "command not executed: %s - queue size: %d", + command, + queueSize); + } + + public static void logClearPendingCommands(@NonNull Object commandQueue) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "clearing pending commands: %s", commandQueue); + } + + public static void logNoPendingCommands() { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "no pending commands to be executed."); + } + + public static void logExecutingCommand(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "executing command: %s", command); + } + + public static void logExecutingCommand(@NonNull Object command, @Nullable Object recentsView) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "executing command: %s - visibleRecentsView: %s", + command, + recentsView); + } + + public static void logExecutedCommandWithResult(@NonNull Object command, boolean isCompleted) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "command executed: %s with result: %b", + command, + isCompleted); + } + + public static void logWaitingForCommandCallback(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "waiting for command callback: %s", command); + } + + public static void logLaunchingTaskCallback(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "launching task callback: %s", command); + } + + public static void logLaunchingTaskWaitingForCallback(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "launching task - waiting for callback: %s", command); + } + + public static void logSwitchingToOverviewStateStart(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "switching to Overview state - onAnimationStart: %s", + command); + } + + public static void logSwitchingToOverviewStateEnd(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "switching to Overview state - onAnimationEnd: %s", + command); + } + + public static void logSwitchingToOverviewStateWaiting(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "switching to Overview state - waiting: %s", command); + } + + public static void logRecentsAnimStarted(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "recents animation started: %s", command); + } + + public static void logOnInitBackgroundStateUI(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "recents animation started - onInitBackgroundStateUI: %s", command); + } + + public static void logRecentsAnimCanceled(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "recents animation canceled: %s", command); + } + + public static void logSwitchingViaRecentsAnim(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "switching via recents animation - onGestureStarted: %s", command); + } + + public static void logSwitchingViaRecentsAnimComplete(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "switching via recents animation - onTransitionComplete: %s", command); + } + + public static void logCommandFinishedButNotScheduled(@Nullable Object nextCommandInQueue, + @NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "next task not scheduled. First pending command type is %s - command type is: %s", + nextCommandInQueue, + command); + } + + public static void logCommandFinishedSuccessfully(@NonNull Object command) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, "command executed successfully: %s", command); + } + + public static void logCommandCanceled(@NonNull Object command, @Nullable Throwable throwable) { + if (!isProtoLogInitialized()) return; + ProtoLog.e(OVERVIEW_COMMAND_HELPER, "command canceled: %s - %s", command, throwable); + } + + public static void logOnNewIntent(boolean alreadyOnHome, boolean shouldMoveToDefaultScreen, + String intentAction, boolean internalStateHandled) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(OVERVIEW_COMMAND_HELPER, + "Launcher.onNewIntent: alreadyOnHome: %b, shouldMoveToDefaultScreen: %b, " + + "intentAction: %s, internalStateHandled: %b", + alreadyOnHome, + shouldMoveToDefaultScreen, + intentAction, + internalStateHandled); + } +} diff --git a/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java new file mode 100644 index 0000000000..8221629aeb --- /dev/null +++ b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java @@ -0,0 +1,78 @@ +/* + * 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.launcher3.util; + +import static com.android.quickstep.util.QuickstepProtoLogGroup.LAUNCHER_STATE_MANAGER; +import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized; + +import android.window.DesktopModeFlags.DesktopModeFlag; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.ProtoLog; +import com.android.launcher3.Flags; + +/** + * Proxy class used for StateManager ProtoLog support. + */ +public class StateManagerProtoLogProxy { + private static final DesktopModeFlag ENABLE_STATE_MANAGER_PROTO_LOG = + new DesktopModeFlag(Flags::enableStateManagerProtoLog, true); + public static void logGoToState( + @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, + "StateManager.goToState: fromState: %s, toState: %s, partial trace:\n%s", + fromState, + toState, + trace); + } + + public static void logCreateAtomicAnimation( + @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.createAtomicAnimation: " + + "fromState: %s, toState: %s, partial trace:\n%s", + fromState, + toState, + trace); + } + + public static void logOnStateTransitionStart(@NonNull Object state) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionStart: state: %s", state); + } + + public static void logOnStateTransitionEnd(@NonNull Object state) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionEnd: state: %s", state); + } + + public static void logOnRepeatStateSetAborted(@NonNull Object state) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, + "StateManager.onRepeatStateSetAborted: state: %s", state); + } + + public static void logCancelAnimation(boolean animationOngoing, @NonNull String trace) { + if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(LAUNCHER_STATE_MANAGER, + "StateManager.cancelAnimation: animation ongoing: %b, partial trace:\n%s", + animationOngoing, + trace); + } +} diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java similarity index 93% rename from quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java index cfa6b9891f..799ba77a36 100644 --- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java +++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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. @@ -36,10 +36,12 @@ public class ActiveGestureErrorDetector { SET_END_TARGET_NEW_TASK, SET_END_TARGET_ALL_APPS, ON_SETTLED_ON_END_TARGET, ON_START_RECENTS_ANIMATION, ON_FINISH_RECENTS_ANIMATION, ON_CANCEL_RECENTS_ANIMATION, START_RECENTS_ANIMATION, FINISH_RECENTS_ANIMATION, CANCEL_RECENTS_ANIMATION, - SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT, + SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION, SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED, FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING, INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING, + QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED, NAVIGATION_MODE_SWITCHED, + RECENTS_ANIMATION_START_TIMEOUT, /** * These GestureEvents are specifically associated to state flags that get set in @@ -125,15 +127,6 @@ public class ActiveGestureErrorDetector { + "before/without startRecentsAnimation.", writer); break; - case CLEANUP_SCREENSHOT: - errorDetected |= printErrorIfTrue( - !encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED), - prefix, - /* errorMessage= */ "recents activity screenshot was " - + "cleaned up before/without STATE_SCREENSHOT_CAPTURED " - + "being set.", - writer); - break; case SCROLLER_ANIMATION_ABORTED: errorDetected |= printErrorIfTrue( encounteredEvents.contains(GestureEvent.SET_END_TARGET_HOME) @@ -282,6 +275,36 @@ public class ActiveGestureErrorDetector { + " animation is still pending.", writer); break; + case QUICK_SWITCH_FROM_HOME_FALLBACK: + errorDetected |= printErrorIfTrue( + true, + prefix, + /* errorMessage= */ "Quick switch from home fallback case: the " + + "TaskView at the current page index was missing.", + writer); + break; + case QUICK_SWITCH_FROM_HOME_FAILED: + errorDetected |= printErrorIfTrue( + true, + prefix, + /* errorMessage= */ "Quick switch from home failed: the TaskViews at " + + "the current page index and index 0 were missing.", + writer); + break; + case NAVIGATION_MODE_SWITCHED: + errorDetected |= printErrorIfTrue( + true, + prefix, + /* errorMessage= */ "Navigation mode switched mid-gesture.", + writer); + break; + case RECENTS_ANIMATION_START_TIMEOUT: + errorDetected |= printErrorIfTrue( + true, + prefix, + /* errorMessage= */ "Recents animation start timed out.", + writer); + break; case EXPECTING_TASK_APPEARED: case MOTION_DOWN: case SET_END_TARGET: @@ -396,15 +419,6 @@ public class ActiveGestureErrorDetector { + "wasn't called and STATE_HANDLER_INVALIDATED wasn't set.", writer); - errorDetected |= printErrorIfTrue( - /* condition= */ encounteredEvents.contains( - GestureEvent.STATE_RECENTS_ANIMATION_CANCELED) - && !encounteredEvents.contains(GestureEvent.CLEANUP_SCREENSHOT), - prefix, - /* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED was set but " - + "the task screenshot wasn't cleaned up.", - writer); - errorDetected |= printErrorIfTrue( /* condition= */ encounteredEvents.contains( GestureEvent.EXPECTING_TASK_APPEARED) diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java similarity index 77% rename from quickstep/src/com/android/quickstep/util/ActiveGestureLog.java rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java index c54862aba3..23e245c51a 100644 --- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java +++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -18,11 +18,11 @@ package com.android.quickstep.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.util.Preconditions; - import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -71,14 +71,6 @@ public class ActiveGestureLog { addLog(event, null); } - public void addLog(@NonNull String event, int extras) { - addLog(event, extras, null); - } - - public void addLog(@NonNull String event, boolean extras) { - addLog(event, extras, null); - } - /** * Adds a log to be printed at log-dump-time and track the associated event for error detection. * @@ -89,20 +81,6 @@ public class ActiveGestureLog { addLog(new CompoundString(event), gestureEvent); } - public void addLog( - @NonNull String event, - int extras, - @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { - addLog(new CompoundString(event).append(": ").append(extras), gestureEvent); - } - - public void addLog( - @NonNull String event, - boolean extras, - @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { - addLog(new CompoundString(event).append(": ").append(extras), gestureEvent); - } - public void addLog(@NonNull CompoundString compoundString) { addLog(compoundString, null); } @@ -237,7 +215,8 @@ public class ActiveGestureLog { /** An entire log of entries associated with a single log ID */ protected static class EventLog { - protected final List eventEntries = new ArrayList<>(); + protected final List eventEntries = + Collections.synchronizedList(new ArrayList<>()); protected final int logId; protected final boolean mIsFullyGesturalNavMode; @@ -250,25 +229,27 @@ public class ActiveGestureLog { /** A buildable string stored as an array for memory efficiency. */ public static class CompoundString { - public static final CompoundString NO_OP = new CompoundString(); + public static final CompoundString NO_OP = new CompoundString(true); private final List mSubstrings; private final List mArgs; private final boolean mIsNoOp; - private CompoundString() { - this(null); + public static CompoundString newEmptyString() { + return new CompoundString(false); } - public CompoundString(String substring) { - mIsNoOp = substring == null; + private CompoundString(boolean isNoOp) { + mIsNoOp = isNoOp; mSubstrings = mIsNoOp ? null : new ArrayList<>(); mArgs = mIsNoOp ? null : new ArrayList<>(); + } - if (!mIsNoOp) { - mSubstrings.add(substring); - } + public CompoundString(String substring, Object... args) { + this(substring == null); + + append(substring, args); } public CompoundString append(CompoundString substring) { @@ -281,80 +262,24 @@ public class ActiveGestureLog { return this; } - public CompoundString append(String substring) { + public CompoundString append(String substring, Object... args) { if (mIsNoOp) { return this; } mSubstrings.add(substring); + mArgs.addAll(Arrays.stream(args).toList()); return this; } - public CompoundString append(int num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%d"); - } - - public CompoundString append(long num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%d"); - } - - public CompoundString append(float num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%.2f"); - } - - public CompoundString append(double num) { - if (mIsNoOp) { - return this; - } - mArgs.add(num); - - return append("%.2f"); - } - - public CompoundString append(boolean bool) { - if (mIsNoOp) { - return this; - } - mArgs.add(bool); - - return append("%b"); - } - - private Object[] getArgs() { - Preconditions.assertTrue(!mIsNoOp); - - return mArgs.toArray(); - } - @Override public String toString() { - return String.format(toUnformattedString(), getArgs()); - } - - private String toUnformattedString() { - Preconditions.assertTrue(!mIsNoOp); - + if (mIsNoOp) return null; StringBuilder sb = new StringBuilder(); for (String substring : mSubstrings) { sb.append(substring); } - - return sb.toString(); + return String.format(sb.toString(), mArgs.toArray()); } @Override @@ -364,10 +289,9 @@ public class ActiveGestureLog { @Override public boolean equals(Object obj) { - if (!(obj instanceof CompoundString)) { + if (!(obj instanceof CompoundString other)) { return false; } - CompoundString other = (CompoundString) obj; return (mIsNoOp == other.mIsNoOp) && Objects.equals(mSubstrings, other.mSubstrings) && Objects.equals(mArgs, other.mArgs); diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java new file mode 100644 index 0000000000..1990c348b5 --- /dev/null +++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java @@ -0,0 +1,594 @@ +/* + * 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.quickstep.util; + +import static android.view.MotionEvent.ACTION_DOWN; + +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_CANCEL_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_FINISH_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_START_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_TIMEOUT; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION; +import static com.android.quickstep.util.QuickstepProtoLogGroup.ACTIVE_GESTURE_LOG; +import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized; + +import android.graphics.Point; +import android.graphics.RectF; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; + +/** + * Proxy class used for ActiveGestureLog ProtoLog support. + *

+ * This file will have all of its static strings in the + * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings. + *

+ * When a new ActiveGestureLog entry needs to be added to the codebase (or and existing entry needs + * to be modified), add it here under a new unique method and make sure the ProtoLog entry matches + * to avoid confusion. + */ +public class ActiveGestureProtoLogProxy { + + public static void logLauncherDestroyed() { + ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED); + if (isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed"); + } + + public static void logAbsSwipeUpHandlerOnRecentsAnimationCanceled() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "AbsSwipeUpHandler.onRecentsAnimationCanceled", + /* gestureEvent= */ CANCEL_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onRecentsAnimationCanceled"); + } + + public static void logAbsSwipeUpHandlerOnRecentsAnimationFinished() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "RecentsAnimationCallbacks.onAnimationFinished", + ON_FINISH_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onAnimationFinished"); + } + + public static void logAbsSwipeUpHandlerCancelCurrentAnimation() { + ActiveGestureLog.INSTANCE.addLog( + "AbsSwipeUpHandler.cancelCurrentAnimation", + ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.cancelCurrentAnimation"); + } + + public static void logAbsSwipeUpHandlerOnTasksAppeared() { + ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.onTasksAppeared: " + + "force finish recents animation complete; clearing state callback."); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onTasksAppeared: " + + "force finish recents animation complete; clearing state callback."); + } + + public static void logHandOffAnimation() { + ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.handOffAnimation"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.handOffAnimation"); + } + + public static void logFinishRecentsAnimationOnTasksAppeared() { + ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimationOnTasksAppeared"); + } + + public static void logRecentsAnimationCallbacksOnAnimationCancelled() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled", + /* gestureEvent= */ ON_CANCEL_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationCanceled"); + } + + public static void logRecentsAnimationCallbacksOnTasksAppeared() { + ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared", + ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onTasksAppeared"); + } + + public static void logStartRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "TaskAnimationManager.startRecentsAnimation", + /* gestureEvent= */ START_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "TaskAnimationManager.startRecentsAnimation"); + } + + public static void logLaunchingSideTaskFailed() { + ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Unable to launch side task (no recents)"); + } + + public static void logContinueRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "continueRecentsAnimation"); + } + + public static void logCleanUpRecentsAnimationSkipped() { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation skipped due to wrong callbacks"); + } + + public static void logCleanUpRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation"); + } + + public static void logOnInputEventUserLocked(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Cannot process input event: user is locked", + displayId)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Cannot process input event: user is locked", + displayId); + } + + public static void logOnInputIgnoringFollowingEvents(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onMotionEvent(displayId=%d): A new gesture has been started, " + + "but a previously-requested recents animation hasn't started. " + + "Ignoring all following motion events.", displayId), + RECENTS_ANIMATION_START_PENDING); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onMotionEvent(displayId=%d): A new gesture has been started, " + + "but a previously-requested recents animation hasn't started. " + + "Ignoring all following motion events.", displayId); + } + + public static void logOnInputEventThreeButtonNav(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "using 3-button nav and event is not a trackpad event", displayId)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "using 3-button nav and event is not a trackpad event", displayId); + } + + public static void logPreloadRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog("preloadRecentsAnimation"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "preloadRecentsAnimation"); + } + + public static void logRecentTasksMissing() { + ActiveGestureLog.INSTANCE.addLog("Null mRecentTasks", RECENT_TASKS_MISSING); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Null mRecentTasks"); + } + + public static void logFinishRecentsAnimationCallback() { + ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation-callback"); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation-callback"); + } + + public static void logOnScrollerAnimationAborted() { + ActiveGestureLog.INSTANCE.addLog("scroller animation aborted", + ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "scroller animation aborted"); + } + + public static void logInputConsumerBecameActive(@NonNull String consumerName) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "%s became active", consumerName)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "%s became active", consumerName); + } + + public static void logTaskLaunchFailed(int launchedTaskId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Launch failed, task (id=%d) finished mid transition", launchedTaskId)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Launch failed, task (id=%d) finished mid transition", launchedTaskId); + } + + public static void logOnPageEndTransition(int nextPageIndex) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onPageEndTransition: current page index updated: %d", nextPageIndex)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onPageEndTransition: current page index updated: %d", nextPageIndex); + } + + public static void logQuickSwitchFromHomeFallback(int taskIndex) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Quick switch from home fallback case: The TaskView at index %d is missing.", + taskIndex), + QUICK_SWITCH_FROM_HOME_FALLBACK); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Quick switch from home fallback case: The TaskView at index %d is missing.", + taskIndex); + } + + public static void logQuickSwitchFromHomeFailed(int taskIndex) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Quick switch from home failed: TaskViews at indices %d and 0 are missing.", + taskIndex), + QUICK_SWITCH_FROM_HOME_FAILED); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Quick switch from home failed: TaskViews at indices %d and 0 are missing.", + taskIndex); + } + + public static void logFinishRecentsAnimation(boolean toRecents) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "finishRecentsAnimation: %b", toRecents), + /* gestureEvent= */ FINISH_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation: %b", toRecents); + } + + public static void logSetEndTarget(@NonNull String target) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "setEndTarget %s", target), /* gestureEvent= */ SET_END_TARGET); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "setEndTarget %s", target); + } + + public static void logStartHomeIntent(@NonNull String reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "OverviewComponentObserver.startHomeIntent: %s", reason)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewComponentObserver.startHomeIntent: %s", reason); + } + + public static void logRunningTaskPackage(@NonNull String packageName) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Current running task package name=%s", packageName)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Current running task package name=%s", packageName); + } + + public static void logSysuiStateFlags(@NonNull String stateFlags) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Current SystemUi state flags=%s", stateFlags)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Current SystemUi state flags=%s", stateFlags); + } + + public static void logSetInputConsumer(@NonNull String consumerName, @NonNull String reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "setInputConsumer: %s. reason(s):%s", consumerName, reason)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "setInputConsumer: %s. reason(s):%s", consumerName, reason); + } + + public static void logUpdateGestureStateRunningTask( + @NonNull String otherTaskPackage, @NonNull String runningTaskPackage) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Changing active task to %s because the previous task running on top of this " + + "one (%s) was excluded from recents", + otherTaskPackage, + runningTaskPackage)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Changing active task to %s because the previous task running on top of this " + + "one (%s) was excluded from recents", + otherTaskPackage, + runningTaskPackage); + } + + public static void logOnInputEventActionUp( + int x, int y, int action, @NonNull String classification, int displayId) { + String actionString = MotionEvent.actionToString(action); + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onMotionEvent(%d, %d): %s, %s, displayId=%d", + x, + y, + actionString, + classification, + displayId), + /* gestureEvent= */ action == ACTION_DOWN + ? MOTION_DOWN + : MOTION_UP); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onMotionEvent(%d, %d): %s, %s, displayId=%d", + x, + y, + actionString, + classification, + displayId); + } + + public static void logOnInputEventActionMove( + @NonNull String action, + @NonNull String classification, + int pointerCount, + int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onMotionEvent: %s, %s, pointerCount: %d, displayId=%d", + action, + classification, + pointerCount, + displayId), + MOTION_MOVE); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onMotionEvent: %s, %s, pointerCount: %d, displayId=%d", + action, + classification, + pointerCount, + displayId); + } + + public static void logOnInputEventGenericAction( + @NonNull String action, @NonNull String classification, int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onMotionEvent: %s, %s, displayId=%d", action, classification, displayId)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "onMotionEvent: %s, %s, displayId=%d", action, classification, displayId); + } + + public static void logOnInputEventNavModeSwitched( + int displayId, @NonNull String startNavMode, @NonNull String currentNavMode) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Navigation mode switched mid-gesture (%s -> %s); " + + "cancelling gesture.", + displayId, + startNavMode, + currentNavMode), + NAVIGATION_MODE_SWITCHED); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Navigation mode switched mid-gesture (%s -> %s); " + + "cancelling gesture.", + displayId, + startNavMode, + currentNavMode); + } + + public static void logUnknownInputEvent(int displayId, @NonNull String event) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "received unknown event %s", displayId, event)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onInputEvent(displayId=%d): Cannot process input event: " + + "received unknown event %s", displayId, event); + } + + public static void logFinishRunningRecentsAnimation(boolean toHome) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "finishRunningRecentsAnimation: %b", toHome)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRunningRecentsAnimation: %b", toHome); + } + + public static void logOnRecentsAnimationStartCancelled() { + ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onAnimationStart (canceled): 0", + /* gestureEvent= */ ON_START_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationStart (canceled): 0"); + } + + public static void logOnRecentsAnimationStart(int appCount) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "RecentsAnimationCallbacks.onAnimationStart: %d", appCount), + /* gestureEvent= */ ON_START_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "RecentsAnimationCallbacks.onAnimationStart: %d", appCount); + } + + public static void logStartRecentsAnimationCallback(@NonNull String callback) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TaskAnimationManager.startRecentsAnimation(%s): " + + "Setting mRecentsAnimationStartPending = false", + callback)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TaskAnimationManager.startRecentsAnimation(%s): " + + "Setting mRecentsAnimationStartPending = false", + callback); + } + + public static void logSettingRecentsAnimationStartPending(boolean value) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TaskAnimationManager.startRecentsAnimation: " + + "Setting mRecentsAnimationStartPending = %b", + value)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TaskAnimationManager.startRecentsAnimation: " + + "Setting mRecentsAnimationStartPending = %b", + value); + } + + public static void logLaunchingSideTask(int taskId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Launching side task id=%d", taskId)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Launching side task id=%d", taskId); + } + + public static void logOnInputEventActionDown( + int displayId, @NonNull ActiveGestureLog.CompoundString reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TIS.onMotionEvent(displayId=%d): ", displayId).append(reason)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "TIS.onMotionEvent(displayId=%d): %s", displayId, reason.toString()); + } + + public static void logStartNewTask(@NonNull ActiveGestureLog.CompoundString tasks) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Launching task: ").append(tasks)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", tasks.toString()); + } + + public static void logMotionPauseDetectorEvent(@NonNull ActiveGestureLog.CompoundString event) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "MotionPauseDetector: ").append(event)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "MotionPauseDetector: %s", event.toString()); + } + + public static void logHandleTaskAppearedFailed( + @NonNull ActiveGestureLog.CompoundString reason) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "handleTaskAppeared check failed: ").append(reason)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "handleTaskAppeared check failed: %s", reason.toString()); + } + + /** + * This is for special cases where the string is purely dynamic and therefore has no format that + * can be extracted. Do not use in any other case. + */ + public static void logDynamicString( + @NonNull String string, + @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { + ActiveGestureLog.INSTANCE.addLog(string, gestureEvent); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "%s", string); + } + + public static void logOnSettledOnEndTarget(@NonNull String endTarget) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "onSettledOnEndTarget %s", endTarget), + /* gestureEvent= */ ON_SETTLED_ON_END_TARGET); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "onSettledOnEndTarget %s", endTarget); + } + + public static void logOnCalculateEndTarget(float velocityX, float velocityY, double angle) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f", + velocityX, + velocityY, + angle), + velocityX == 0 && velocityY == 0 ? INVALID_VELOCITY_ON_SWIPE_UP : null); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f", + velocityX, + velocityY, + angle); + } + + public static void logUnexpectedTaskAppeared(int taskId, @NonNull String packageName) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s", + taskId, + packageName)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s", + taskId, + packageName); + } + + public static void logCreateTouchRegionForDisplay(int displayRotation, + @NonNull Point displaySize, @NonNull RectF swipeRegion, @NonNull RectF ohmRegion, + int gesturalHeight, int largerGesturalHeight, @NonNull String reason) { + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "OrientationTouchTransformer.createRegionForDisplay: " + + "dispRot=%d, dispSize=%s, swipeRegion=%s, ohmRegion=%s, " + + "gesturalHeight=%d, largerGesturalHeight=%d, reason=%s", + displayRotation, displaySize.flattenToString(), swipeRegion.toShortString(), + ohmRegion.toShortString(), gesturalHeight, largerGesturalHeight, reason); + } + + public static void logOnTaskAnimationManagerNotAvailable(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "TaskAnimationManager not available for displayId=%d", + displayId)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "TaskAnimationManager not available for displayId=%d", + displayId); + } + + public static void logOnAbsSwipeUpHandlerNotAvailable(int displayId) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "AbsSwipeUpHandler not available for displayId=%d", + displayId)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler not available for displayId=%d", + displayId); + } + + public static void logGestureStartSwipeHandler(@NonNull String interactionHandler) { + ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString( + "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation: " + + "interactionHandler=%s", interactionHandler)); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, + "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation: " + + "interactionHandler=%s", interactionHandler); + } + + public static void logQueuingForceFinishRecentsAnimation() { + ActiveGestureLog.INSTANCE.addLog("Launcher destroyed while mRecentsAnimationStartPending ==" + + " true, queuing a callback to clean the pending animation up on start", + /* gestureEvent= */ ON_START_RECENTS_ANIMATION); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed while mRecentsAnimationStartPending ==" + + " true, queuing a callback to clean the pending animation up on start"); + } + + public static void logRecentsAnimationStartTimedOut() { + ActiveGestureLog.INSTANCE.addLog("Recents animation start has timed out; forcefully " + + "cleaning up the recents animation.", + /* gestureEvent= */ RECENTS_ANIMATION_START_TIMEOUT); + if (!isProtoLogInitialized()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "Recents animation start has timed out; forcefully " + + "cleaning up the recents animation."); + } +} diff --git a/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java new file mode 100644 index 0000000000..7696a70a07 --- /dev/null +++ b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java @@ -0,0 +1,136 @@ +/* + * 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.quickstep.util; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; + +import java.util.UUID; + +/** Enums used to interface with the ProtoLog API. */ +public enum QuickstepProtoLogGroup implements IProtoLogGroup { + + ACTIVE_GESTURE_LOG(true, true, Constants.DEBUG_ACTIVE_GESTURE, "ActiveGestureLog"), + RECENTS_WINDOW(true, true, Constants.DEBUG_RECENTS_WINDOW, "RecentsWindow"), + LAUNCHER_STATE_MANAGER(true, true, Constants.DEBUG_STATE_MANAGER, "LauncherStateManager"), + OVERVIEW_COMMAND_HELPER(true, true, Constants.DEBUG_OVERVIEW_COMMAND_HELPER, + "OverviewCommandHelper"); + + private final boolean mEnabled; + private volatile boolean mLogToProto; + private volatile boolean mLogToLogcat; + private final @NonNull String mTag; + + public static boolean isProtoLogInitialized() { + if (!Variables.sIsInitialized) { + Log.w(Constants.TAG, + "Attempting to log to ProtoLog before initializing it.", + new IllegalStateException()); + } + return Variables.sIsInitialized; + } + + public static void initProtoLog() { + if (Variables.sIsInitialized) { + Log.e(Constants.TAG, + "Attempting to re-initialize ProtoLog.", new IllegalStateException()); + return; + } + Log.i(Constants.TAG, "Initializing ProtoLog."); + Variables.sIsInitialized = true; + ProtoLog.init(QuickstepProtoLogGroup.values()); + } + + /** + * @param enabled set to false to exclude all log statements for this group from + * compilation, + * they will not be available in runtime. + * @param logToProto enable binary logging for the group + * @param logToLogcat enable text logging for the group + * @param tag name of the source of the logged message + */ + QuickstepProtoLogGroup( + boolean enabled, boolean logToProto, boolean logToLogcat, @NonNull String tag) { + this.mEnabled = enabled; + this.mLogToProto = logToProto; + this.mLogToLogcat = logToLogcat; + this.mTag = tag; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public boolean isLogToProto() { + return mLogToProto; + } + + @Override + public boolean isLogToLogcat() { + return mLogToLogcat; + } + + @Override + public boolean isLogToAny() { + return mLogToLogcat || mLogToProto; + } + + @Override + public int getId() { + return Constants.LOG_START_ID + this.ordinal(); + } + + @Override + public @NonNull String getTag() { + return mTag; + } + + @Override + public void setLogToProto(boolean logToProto) { + this.mLogToProto = logToProto; + } + + @Override + public void setLogToLogcat(boolean logToLogcat) { + this.mLogToLogcat = logToLogcat; + } + + private static final class Variables { + + private static boolean sIsInitialized = false; + } + + private static final class Constants { + + private static final String TAG = "QuickstepProtoLogGroup"; + + private static final boolean DEBUG_ACTIVE_GESTURE = false; + private static final boolean DEBUG_RECENTS_WINDOW = false; + private static final boolean DEBUG_STATE_MANAGER = true; // b/279059025, b/325463989 + private static final boolean DEBUG_OVERVIEW_COMMAND_HELPER = true; + + private static final int LOG_START_ID = + (int) (UUID.nameUUIDFromBytes(QuickstepProtoLogGroup.class.getName().getBytes()) + .getMostSignificantBits() % Integer.MAX_VALUE); + } +} diff --git a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java new file mode 100644 index 0000000000..41e9cefbc1 --- /dev/null +++ b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java @@ -0,0 +1,74 @@ +/* + * 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.quickstep.util; + +import static com.android.quickstep.util.QuickstepProtoLogGroup.RECENTS_WINDOW; +import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized; + +import android.window.DesktopExperienceFlags; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; +import com.android.launcher3.Flags; + +/** + * Proxy class used for Recents Window ProtoLog support. + *

+ * This file will have all of its static strings in the + * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings. + *

+ * When a new Recents Window log needs to be added to the codebase, add it here under a new unique + * method. Or, if an existing entry needs to be modified, simply update it here. + */ +public class RecentsWindowProtoLogProxy { + private static final DesktopExperienceFlags.DesktopExperienceFlag + ENABLE_RECENTS_WINDOW_PROTO_LOG = + new DesktopExperienceFlags.DesktopExperienceFlag( + Flags::enableRecentsWindowProtoLog, + false, + Flags.FLAG_ENABLE_RECENTS_WINDOW_PROTO_LOG); + + public static void logOnStateSetStart(@NonNull String stateName) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, "onStateSetStart: %s", stateName); + } + + public static void logOnStateSetEnd(@NonNull String stateName) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, "onStateSetEnd: %s", stateName); + } + + public static void logOnRepeatStateSetAborted(@NonNull String stateName) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, "onRepeatStateSetAborted: %s", stateName); + } + + public static void logStartRecentsWindow(boolean isShown, boolean windowViewIsNull) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, + "Starting recents window: isShow= %b, windowViewIsNull=%b", + isShown, + windowViewIsNull); + } + + public static void logCleanup(boolean isShown) { + if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return; + ProtoLog.d(RECENTS_WINDOW, "Cleaning up recents window: isShow= %b", isShown); + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt new file mode 100644 index 0000000000..3d83740163 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt @@ -0,0 +1,160 @@ +/* + * 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.launcher3.taskbar.bubbles + +import android.content.Context +import android.graphics.Color +import android.platform.test.rule.ScreenRecordRule +import android.view.View +import android.widget.FrameLayout +import android.widget.FrameLayout.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout.LayoutParams.WRAP_CONTENT +import androidx.activity.ComponentActivity +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.R +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +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 + +/** Screenshot tests for [BubbleBarView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +@ScreenRecordRule.ScreenRecord +class BubbleBarViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleBarView: BubbleBarView + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun bubbleBarView_collapsed_oneBubble() { + screenshotRule.screenshotTest("bubbleBarView_collapsed_oneBubble") { activity -> + activity.actionBar?.hide() + setupBubbleBarView() + bubbleBarView.addBubble(createBubble("key1", Color.GREEN), false) + val container = FrameLayout(context) + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + container.layoutParams = lp + container.addView(bubbleBarView) + container + } + } + + @Test + fun bubbleBarView_collapsed_twoBubbles() { + screenshotRule.screenshotTest("bubbleBarView_collapsed_twoBubbles") { activity -> + activity.actionBar?.hide() + setupBubbleBarView() + bubbleBarView.addBubble(createBubble("key1", Color.GREEN), true) + bubbleBarView.addBubble(createBubble("key2", Color.CYAN), true) + val container = FrameLayout(context) + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + container.layoutParams = lp + container.addView(bubbleBarView) + container + } + } + + @Test + fun bubbleBarView_expanded_threeBubbles() { + // if we're still expanding, wait with taking a screenshot + val shouldWait: (ComponentActivity, View) -> Boolean = { _, _ -> bubbleBarView.isExpanding } + // increase the frame limit to allow the animation to end before taking the screenshot + screenshotRule.frameLimit = 500 + screenshotRule.screenshotTest( + "bubbleBarView_expanded_threeBubbles", + checkView = shouldWait, + ) { activity -> + activity.actionBar?.hide() + setupBubbleBarView() + bubbleBarView.addBubble(createBubble("key1", Color.GREEN), false) + bubbleBarView.addBubble(createBubble("key2", Color.CYAN), false) + bubbleBarView.addBubble(createBubble("key3", Color.MAGENTA), false) + val container = FrameLayout(context) + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + container.layoutParams = lp + container.addView(bubbleBarView) + bubbleBarView.animateExpanded(true) + container + } + } + + private fun setupBubbleBarView() { + bubbleBarView = BubbleBarView(context) + val lp = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + bubbleBarView.layoutParams = lp + val paddingTop = + context.resources.getDimensionPixelSize(R.dimen.bubblebar_pointer_visible_size) + bubbleBarView.setPadding(0, paddingTop, 0, 0) + bubbleBarView.setController( + object : BubbleBarView.Controller { + override fun getBubbleBarTranslationY(): Float = 0f + + override fun onBubbleBarTouched() {} + + override fun expandBubbleBar() {} + + override fun dismissBubbleBar() {} + + override fun updateBubbleBarLocation(location: BubbleBarLocation?, source: Int) {} + + override fun setIsDragging(dragging: Boolean) {} + + override fun onBubbleBarExpandedStateChanged(expanded: Boolean) {} + } + ) + bubbleBarView.visibility = View.VISIBLE + bubbleBarView.alpha = 1f + } + + private fun createBubble(key: String, color: Int): BubbleView { + val bubbleView = + FakeBubbleViewFactory.createBubble( + context, + key, + parent = bubbleBarView, + iconColor = color, + ) + bubbleView.showDotIfNeeded(1f) + bubbleBarView.setSelectedBubble(bubbleView) + return bubbleView + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt new file mode 100644 index 0000000000..0661a99dbd --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewScreenshotTest.kt @@ -0,0 +1,93 @@ +/* + * 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.launcher3.taskbar.bubbles + +import android.content.Context +import android.graphics.Color +import androidx.test.core.app.ApplicationProvider +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +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 + +/** Screenshot tests for [BubbleView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + private val context = ApplicationProvider.getApplicationContext() + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun bubbleView_hasUnseenContent() { + screenshotRule.screenshotTest("bubbleView_hasUnseenContent") { activity -> + activity.actionBar?.hide() + setupBubbleView() + } + } + + @Test + fun bubbleView_seen() { + screenshotRule.screenshotTest("bubbleView_seen") { activity -> + activity.actionBar?.hide() + setupBubbleView(suppressNotification = true) + } + } + + @Test + fun bubbleView_badgeHidden() { + screenshotRule.screenshotTest("bubbleView_badgeHidden") { activity -> + activity.actionBar?.hide() + setupBubbleView().apply { setBadgeScale(0f) } + } + } + + private fun setupBubbleView(suppressNotification: Boolean = false): BubbleView { + val bubbleView = + FakeBubbleViewFactory.createBubble( + context, + key = "key", + parent = null, + iconSize = 100, + iconColor = Color.LTGRAY, + suppressNotification = suppressNotification, + ) + bubbleView.showDotIfNeeded(1f) + return bubbleView + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/FakeBubbleViewFactory.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/FakeBubbleViewFactory.kt new file mode 100644 index 0000000000..e6d3ae7661 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/FakeBubbleViewFactory.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles + +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.PathParser +import android.view.LayoutInflater +import android.view.ViewGroup +import com.android.launcher3.R +import com.android.wm.shell.shared.bubbles.BubbleInfo + +object FakeBubbleViewFactory { + + /** Inflates a [BubbleView] and adds it to the [parent] view if it is present. */ + fun createBubble( + context: Context, + key: String, + parent: ViewGroup?, + iconSize: Int = 50, + iconColor: Int, + badgeColor: Int = Color.RED, + dotColor: Int = Color.BLUE, + suppressNotification: Boolean = false, + ): BubbleView { + val inflater = LayoutInflater.from(context) + // BubbleView uses launcher's badge to icon ratio and expects the badge image to already + // have the right size + val badgeToIconRatio = 0.444f + val badgeRadius = iconSize * badgeToIconRatio / 2 + val icon = createCircleBitmap(radius = iconSize / 2, color = iconColor) + val badge = createCircleBitmap(radius = badgeRadius.toInt(), color = badgeColor) + + val flags = + if (suppressNotification) Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION else 0 + val bubbleInfo = + BubbleInfo( + key, + flags, + null, + null, + 0, + context.packageName, + null, + null, + false, + true, + null, + ) + val bubbleView = inflater.inflate(R.layout.bubblebar_item_view, parent, false) as BubbleView + val dotPath = + PathParser.createPathFromPathData( + context.resources.getString(com.android.internal.R.string.config_icon_mask) + ) + val bubble = + BubbleBarBubble( + bubbleInfo, + bubbleView, + badge, + icon, + dotColor, + dotPath, + "test app", + null, + ) + bubbleView.setBubble(bubble) + return bubbleView + } + + private fun createCircleBitmap(radius: Int, color: Int): Bitmap { + val bitmap = Bitmap.createBitmap(radius * 2, radius * 2, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawARGB(0, 0, 0, 0) + val paint = Paint() + paint.color = color + canvas.drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), paint) + return bitmap + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS new file mode 100644 index 0000000000..63c14984b9 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com +mpodolian@google.com diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt new file mode 100644 index 0000000000..11c7fe9823 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt @@ -0,0 +1,253 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.content.Context +import android.graphics.Color +import android.graphics.PointF +import android.graphics.drawable.ColorDrawable +import androidx.test.core.app.ApplicationProvider +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +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 + +/** Screenshot tests for [BubbleBarFlyoutView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + private val context = ApplicationProvider.getApplicationContext() + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun bubbleBarFlyoutView_noAvatar_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message") + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_noAvatar_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message") + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_noAvatar_longMessage() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_longMessage") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = null, + title = "sender", + message = "really, really, really, really, really long message. like really.", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "message", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "message", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_avatar_longMessage() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_longMessage") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "really, really, really, really, really long message. like really.", + ) + ) {} + flyout.updateExpansionProgress(1f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_collapsed_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "collapsed on left", + ) + ) {} + flyout.updateExpansionProgress(0f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_collapsed_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false)) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "collapsed on right", + ) + ) {} + flyout.updateExpansionProgress(0f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_90p_onLeft() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_90p_onLeft") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView( + context, + FakeBubbleBarFlyoutPositioner( + isOnLeft = true, + distanceToCollapsedPosition = PointF(100f, 100f), + ), + ) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "expanded 90% on left", + ) + ) {} + flyout.updateExpansionProgress(0.9f) + flyout + } + } + + @Test + fun bubbleBarFlyoutView_80p_onRight() { + screenshotRule.screenshotTest("bubbleBarFlyoutView_80p_onRight") { activity -> + activity.actionBar?.hide() + val flyout = + BubbleBarFlyoutView( + context, + FakeBubbleBarFlyoutPositioner( + isOnLeft = false, + distanceToCollapsedPosition = PointF(200f, 100f), + ), + ) + flyout.showFromCollapsed( + BubbleBarFlyoutMessage( + icon = ColorDrawable(Color.RED), + title = "sender", + message = "expanded 80% on right", + ) + ) {} + flyout.updateExpansionProgress(0.8f) + flyout + } + } + + private class FakeBubbleBarFlyoutPositioner( + override val isOnLeft: Boolean, + override val distanceToCollapsedPosition: PointF = PointF(0f, 0f), + ) : BubbleBarFlyoutPositioner { + override val targetTy = 0f + override val collapsedSize = 30f + override val collapsedColor = Color.BLUE + override val collapsedElevation = 1f + override val distanceToRevealTriangle = 10f + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/apptimer/TaskAppTimerScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/apptimer/TaskAppTimerScreenshotTest.kt new file mode 100644 index 0000000000..bef8709769 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/apptimer/TaskAppTimerScreenshotTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.apptimer + +import android.content.Context +import android.graphics.Color +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Gravity +import android.view.LayoutInflater +import android.widget.FrameLayout +import com.android.launcher3.Flags +import com.android.launcher3.R +import com.android.launcher3.util.Themes +import com.android.launcher3.util.rule.setFlags +import com.android.quickstep.task.thumbnail.TaskContentView +import com.android.quickstep.task.thumbnail.TaskHeaderUiState +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import java.time.Duration +import org.junit.Before +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 + +/** Screenshot tests for the digital wellbeing timer shown in the task content view */ +@RunWith(ParameterizedAndroidJunit4::class) +class TaskAppTimerScreenshotTest(emulationSpec: DeviceEmulationSpec) { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Before + fun setUp() { + setFlagsRule.setFlags( + true, + Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, + Flags.FLAG_ENABLE_REFACTOR_TASK_CONTENT_VIEW, + Flags.FLAG_ENABLE_REFACTOR_DIGITAL_WELLBEING_TOAST, + ) + } + + @Test + fun taskAppTimer_iconAndFullText() { + screenshotRule.screenshotTest("taskAppTimer_iconAndFullText") { activity -> + activity.actionBar?.hide() + val container = createContainer(activity) + val taskContentView = createTaskContentView(activity) + container.addView(taskContentView, CONTAINER_WIDTH_WIDE, CONTAINER_HEIGHT) + + taskContentView.setState( + taskHeaderState = TaskHeaderUiState.HideHeader, + taskThumbnailUiState = BackgroundOnly(Color.YELLOW), + taskAppTimerUiState = TIMER_UI_STATE, + taskId = null, + ) + + container + } + } + + @Test + fun taskAppTimer_iconAndShortText() { + screenshotRule.screenshotTest("taskAppTimer_iconAndShortText") { activity -> + activity.actionBar?.hide() + val container = createContainer(activity) + val taskContentView = createTaskContentView(activity) + container.addView(taskContentView, CONTAINER_WIDTH_MEDIUM, CONTAINER_HEIGHT) + + taskContentView.setState( + taskHeaderState = TaskHeaderUiState.HideHeader, + taskThumbnailUiState = BackgroundOnly(Color.YELLOW), + taskAppTimerUiState = TIMER_UI_STATE, + taskId = null, + ) + + container + } + } + + @Test + fun taskAppTimer_iconOnly() { + screenshotRule.screenshotTest("taskAppTimer_iconOnly") { activity -> + activity.actionBar?.hide() + val container = createContainer(activity) + val taskContentView = createTaskContentView(activity) + container.addView(taskContentView, CONTAINER_WIDTH_NARROW, CONTAINER_HEIGHT) + + taskContentView.setState( + taskHeaderState = TaskHeaderUiState.HideHeader, + taskThumbnailUiState = BackgroundOnly(Color.YELLOW), + taskAppTimerUiState = TIMER_UI_STATE, + taskId = null, + ) + + container + } + } + + private fun createTaskContentView(context: Context): TaskContentView { + val taskContentView = + LayoutInflater.from(context).inflate(R.layout.task_content_view, null, false) + as TaskContentView + taskContentView.cornerRadius = Themes.getDialogCornerRadius(context) + return taskContentView + } + + private fun createContainer(context: Context): FrameLayout { + val container = FrameLayout(context) + val lp = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER, + ) + container.layoutParams = lp + + return container + } + + companion object { + private const val CONTAINER_HEIGHT = 700 + private const val CONTAINER_WIDTH_WIDE = 800 + private const val CONTAINER_WIDTH_MEDIUM = 400 + private const val CONTAINER_WIDTH_NARROW = 150 + + private val TIMER_UI_STATE = + TaskAppTimerUiState.Timer( + timeRemaining = Duration.ofHours(23).plusMinutes(2), + taskDescription = "test", + taskPackageName = "com.test", + accessibilityActionId = R.id.action_digital_wellbeing_top_left, + ) + + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/SplashHelper.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/SplashHelper.kt new file mode 100644 index 0000000000..8cc09d470b --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/SplashHelper.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint + +object SplashHelper { + private val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN) + + fun createSplash(): Bitmap = createBitmap(width = 20, height = 20, rectColorRotation = 1) + + fun createBitmap(width: Int, height: Int, rectColorRotation: Int = 0): Bitmap = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply { + Canvas(this).apply { + val paint = Paint() + paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4] + drawRect(0f, 0f, width / 2f, height / 2f, paint) + paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4] + drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint) + paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4] + drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint) + paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4] + drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint) + } + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt new file mode 100644 index 0000000000..41d793f8f5 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskContentViewScreenshotTest.kt @@ -0,0 +1,156 @@ +/* + * 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.quickstep.task.thumbnail + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.platform.test.flag.junit.SetFlagsRule +import android.view.LayoutInflater +import com.android.launcher3.Flags +import com.android.launcher3.R +import com.android.launcher3.util.rule.setFlags +import com.android.quickstep.task.apptimer.TaskAppTimerUiState +import com.android.quickstep.task.thumbnail.SplashHelper.createSplash +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import java.time.Duration +import org.junit.Before +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 + +/** Screenshot tests for [TaskContentView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class TaskContentViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Before + fun setUp() { + setFlagsRule.setFlags( + true, + Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL, + Flags.FLAG_ENABLE_REFACTOR_TASK_CONTENT_VIEW, + Flags.FLAG_ENABLE_REFACTOR_DIGITAL_WELLBEING_TOAST, + ) + } + + @Test + fun taskContentView_recyclesToUninitialized() { + screenshotRule.screenshotTest( + "taskContentView_uninitialized", + ViewScreenshotTestRule.Mode.MatchSize, + ) { activity -> + activity.actionBar?.hide() + val taskContentView = createTaskContentView(activity) + taskContentView.setState( + TaskHeaderUiState.HideHeader, + BackgroundOnly(Color.YELLOW), + TIMER_UI_STATE, + null, + ) + taskContentView.onRecycle() + taskContentView + } + } + + @Test + fun taskContentView_shows_thumbnail_and_header() { + screenshotRule.screenshotTest( + "taskContentView_shows_thumbnail_and_header", + ViewScreenshotTestRule.Mode.MatchSize, + ) { activity -> + activity.actionBar?.hide() + createTaskContentView(activity).apply { + setState( + TaskHeaderUiState.ShowHeader( + TaskHeaderUiState.ThumbnailHeader( + BitmapDrawable(activity.resources, createSplash()), + "test", + ) {} + ), + BackgroundOnly(Color.YELLOW), + NO_TIMER_UI_STATE, + null, + ) + } + } + } + + @Test + fun taskContentView_scaled_roundRoundedCorners() { + screenshotRule.screenshotTest( + "taskContentView_scaledRoundedCorners", + ViewScreenshotTestRule.Mode.MatchSize, + ) { activity -> + activity.actionBar?.hide() + createTaskContentView(activity).apply { + scaleX = 0.75f + scaleY = 0.3f + setState( + TaskHeaderUiState.HideHeader, + BackgroundOnly(Color.YELLOW), + NO_TIMER_UI_STATE, + null, + ) + } + } + } + + private fun createTaskContentView(context: Context): TaskContentView { + val taskContentView = + LayoutInflater.from(context).inflate(R.layout.task_content_view, null, false) + as TaskContentView + taskContentView.cornerRadius = CORNER_RADIUS + return taskContentView + } + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + + const val CORNER_RADIUS = 56f + + private val TIMER_UI_STATE = + TaskAppTimerUiState.Timer( + timeRemaining = Duration.ofHours(2).plusMinutes(20L), + taskDescription = "test", + taskPackageName = "com.test", + accessibilityActionId = R.id.action_digital_wellbeing_top_left, + ) + private val NO_TIMER_UI_STATE = TaskAppTimerUiState.NoTimer(taskDescription = "test") + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskHeaderViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskHeaderViewScreenshotTest.kt new file mode 100644 index 0000000000..7e085457e8 --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskHeaderViewScreenshotTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.thumbnail + +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.view.LayoutInflater +import android.widget.FrameLayout +import android.widget.ImageButton +import com.android.launcher3.R +import com.android.quickstep.task.thumbnail.SplashHelper.createSplash +import com.android.quickstep.views.TaskHeaderView +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +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.ViewScreenshotTestRule.Mode.WrapContent +import platform.test.screenshot.getEmulatedDevicePathConfig + +/** Screenshot tests for [TaskHeaderView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class TaskHeaderViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Test + fun taskHeaderView_showHeader() { + screenshotRule.screenshotTest("taskHeaderView_showHeader", mode = WrapContent) { activity -> + activity.actionBar?.hide() + val container = FrameLayout(activity) + val headerView = createTaskHeaderView(activity) + + container.addView(headerView, CONTAINER_WIDTH, CONTAINER_HEIGHT) + headerView.setState( + TaskHeaderUiState.ShowHeader( + TaskHeaderUiState.ThumbnailHeader( + BitmapDrawable(activity.resources, createSplash()), + "Example", + ) {} + ) + ) + container + } + } + + @Test + fun taskNarrowHeaderView_showHeader() { + screenshotRule.screenshotTest("taskNarrowHeaderView_showHeader", mode = WrapContent) { + activity -> + activity.actionBar?.hide() + val container = FrameLayout(activity) + val headerView = createTaskHeaderView(activity) + + container.addView(headerView, CONTAINER_NARROW_WIDTH, CONTAINER_HEIGHT) + headerView.setState( + TaskHeaderUiState.ShowHeader( + TaskHeaderUiState.ThumbnailHeader( + BitmapDrawable(activity.resources, createSplash()), + "Example", + ) {} + ) + ) + container + } + } + + @Test + fun taskHeaderView_closeButtonHovered() { + screenshotRule.screenshotTest("taskHeaderView_closeButtonHovered", mode = WrapContent) { + activity -> + activity.actionBar?.hide() + val container = FrameLayout(activity) + val headerView = createTaskHeaderView(activity) + + container.addView(headerView, CONTAINER_WIDTH, CONTAINER_HEIGHT) + headerView.setState( + TaskHeaderUiState.ShowHeader( + TaskHeaderUiState.ThumbnailHeader( + BitmapDrawable(activity.resources, createSplash()), + "Example", + ) {} + ) + ) + // Simulate hover state on the close button + val closeButton = headerView.findViewById(R.id.header_close_button) + activity.runOnUiThread { closeButton.isHovered = true } + container + } + } + + @Test + fun taskHeaderView_closeButtonPressed() { + screenshotRule.screenshotTest("taskHeaderView_closeButtonPressed", mode = WrapContent) { + activity -> + activity.actionBar?.hide() + val container = FrameLayout(activity) + val headerView = createTaskHeaderView(activity) + + container.addView(headerView, CONTAINER_WIDTH, CONTAINER_HEIGHT) + headerView.setState( + TaskHeaderUiState.ShowHeader( + TaskHeaderUiState.ThumbnailHeader( + BitmapDrawable(activity.resources, createSplash()), + "Example", + ) {} + ) + ) + // Simulate press state on the close button + val closeButton = headerView.findViewById(R.id.header_close_button) + activity.runOnUiThread { closeButton.isPressed = true } + container + } + } + + private fun createTaskHeaderView(context: Context): TaskHeaderView { + val taskHeaderView = + LayoutInflater.from(context).inflate(R.layout.task_header_view, null, false) + as TaskHeaderView + return taskHeaderView + } + + companion object { + private const val CONTAINER_HEIGHT = 65 + private const val CONTAINER_WIDTH = 400 + private const val CONTAINER_NARROW_WIDTH = 300 + + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Tablet, + isDarkTheme = false, + isLandscape = true, + ) + } +} diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt new file mode 100644 index 0000000000..86bfdcb22a --- /dev/null +++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt @@ -0,0 +1,266 @@ +/* + * 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.quickstep.task.thumbnail + +import android.content.Context +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.drawable.BitmapDrawable +import android.platform.test.flag.junit.SetFlagsRule +import android.view.LayoutInflater +import android.view.Surface.ROTATION_0 +import com.android.launcher3.Flags +import com.android.launcher3.R +import com.android.launcher3.util.rule.setFlags +import com.android.quickstep.task.thumbnail.SplashHelper.createBitmap +import com.android.quickstep.task.thumbnail.SplashHelper.createSplash +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized +import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager +import org.junit.Before +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 + +/** Screenshot tests for [TaskThumbnailView]. */ +@RunWith(ParameterizedAndroidJunit4::class) +class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)), + ) + + @Before + fun setUp() { + setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_REFACTOR_TASK_CONTENT_VIEW) + } + + @Test + fun taskThumbnailView_uninitializedByDefault() { + screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity) + } + } + + @Test + fun taskThumbnailView_resetsToUninitialized() { + screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity -> + activity.actionBar?.hide() + val taskThumbnailView = createTaskThumbnailView(activity) + taskThumbnailView.setState(BackgroundOnly(Color.YELLOW)) + taskThumbnailView.setState(Uninitialized) + taskThumbnailView + } + } + + @Test + fun taskThumbnailView_recyclesToUninitialized() { + screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity -> + activity.actionBar?.hide() + val taskThumbnailView = createTaskThumbnailView(activity) + taskThumbnailView.setState(BackgroundOnly(Color.YELLOW)) + taskThumbnailView.onRecycle() + taskThumbnailView + } + } + + @Test + fun taskThumbnailView_backgroundOnly() { + screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { setState(BackgroundOnly(Color.YELLOW)) } + } + } + + @Test + fun taskThumbnailView_liveTile() { + screenshotRule.screenshotTest("taskThumbnailView_liveTile") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { setState(TaskThumbnailUiState.LiveTile) } + } + } + + @Test + fun taskThumbnailView_image() { + screenshotRule.screenshotTest("taskThumbnailView_image") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState( + SnapshotSplash( + Snapshot( + createBitmap(VIEW_ENV_WIDTH, VIEW_ENV_HEIGHT), + ROTATION_0, + Color.DKGRAY, + ), + null, + ) + ) + } + } + } + + @Test + fun taskThumbnailView_image_withImageMatrix() { + screenshotRule.screenshotTest("taskThumbnailView_image_withMatrix") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200 + setState( + SnapshotSplash( + Snapshot( + createBitmap( + width = VIEW_ENV_WIDTH / 2, + height = lessThanHeightMatchingAspectRatio, + ), + ROTATION_0, + Color.DKGRAY, + ), + null, + ) + ) + setImageMatrix(Matrix().apply { postScale(2f, 2f) }) + } + } + } + + @Test + fun taskThumbnailView_splash() { + screenshotRule.screenshotTest("taskThumbnailView_partial_splash") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState( + SnapshotSplash( + Snapshot( + createBitmap(VIEW_ENV_WIDTH, VIEW_ENV_HEIGHT), + ROTATION_0, + Color.DKGRAY, + ), + BitmapDrawable(activity.resources, createSplash()), + ) + ) + updateSplashAlpha(0.5f) + } + } + } + + @Test + fun taskThumbnailView_splash_withImageMatrix() { + screenshotRule.screenshotTest("taskThumbnailView_partial_splash_withMatrix") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200 + setState( + SnapshotSplash( + Snapshot( + createBitmap( + width = VIEW_ENV_WIDTH / 2, + height = lessThanHeightMatchingAspectRatio, + ), + ROTATION_0, + Color.DKGRAY, + ), + BitmapDrawable(activity.resources, createSplash()), + ) + ) + setImageMatrix(Matrix().apply { postScale(2f, 2f) }) + updateSplashAlpha(0.5f) + } + } + } + + @Test + fun taskThumbnailView_dimmed_tintAmount() { + screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState(BackgroundOnly(Color.YELLOW)) + updateTintAmount(.4f) + } + } + } + + @Test + fun taskThumbnailView_dimmed_menuOpen() { + screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState(BackgroundOnly(Color.YELLOW)) + updateMenuOpenProgress(1f) + } + } + } + + @Test + fun taskThumbnailView_dimmed_tintAmountAndMenuOpen() { + screenshotRule.screenshotTest("taskThumbnailView_dimmed_80") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + setState(BackgroundOnly(Color.YELLOW)) + updateTintAmount(.8f) + updateMenuOpenProgress(1f) + } + } + } + + @Test + fun taskThumbnailView_scaled_roundRoundedCorners() { + screenshotRule.screenshotTest("taskThumbnailView_scaledRoundedCorners") { activity -> + activity.actionBar?.hide() + createTaskThumbnailView(activity).apply { + scaleX = 0.75f + scaleY = 0.3f + setState(BackgroundOnly(Color.YELLOW)) + } + } + } + + private fun createTaskThumbnailView(context: Context): TaskThumbnailView { + val taskThumbnailView = + LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false) + as TaskThumbnailView + taskThumbnailView.cornerRadius = CORNER_RADIUS + return taskThumbnailView + } + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = + DeviceEmulationSpec.forDisplays( + Displays.Phone, + isDarkTheme = false, + isLandscape = false, + ) + + const val CORNER_RADIUS = 56f + const val VIEW_ENV_WIDTH = 1440 + const val VIEW_ENV_HEIGHT = 3120 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt new file mode 100644 index 0000000000..b705beab6e --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt @@ -0,0 +1,124 @@ +/* + * 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.launcher3.desktop + +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.Context +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.TransitionFilter +import android.window.TransitionFilter.CONTAINER_ORDER_ANY +import android.window.TransitionFilter.CONTAINER_ORDER_TOP +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.quickstep.SystemUiProxy +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_BUGFIX +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DesktopAppLaunchTransitionManagerTest { + + @get:Rule val mSetFlagsRule = SetFlagsRule() + + private val context = mock() + private val systemUiProxy = mock() + private lateinit var transitionManager: DesktopAppLaunchTransitionManager + + @Before + fun setUp() { + whenever(context.resources).thenReturn(mock()) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + transitionManager = DesktopAppLaunchTransitionManager(context, systemUiProxy) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + fun registerTransitions_appLaunchFlagEnabled_registersTransition() { + transitionManager.registerTransitions() + + verify(systemUiProxy, times(1)).registerRemoteTransition(any(), any()) + } + + @Test + @DisableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + fun registerTransitions_appLaunchFlagDisabled_doesntRegisterTransition() { + transitionManager.registerTransitions() + + verify(systemUiProxy, times(0)).registerRemoteTransition(any(), any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + @DisableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_BUGFIX) + fun registerTransitions_usesCorrectFilter_flagDisabled() { + transitionManager.registerTransitions() + val filterArgumentCaptor = argumentCaptor() + + verify(systemUiProxy, times(1)) + .registerRemoteTransition(any(), filterArgumentCaptor.capture()) + + assertThat(filterArgumentCaptor.lastValue).isNotNull() + assertThat(filterArgumentCaptor.lastValue.mTypeSet) + .isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)) + assertThat(filterArgumentCaptor.lastValue.mRequirements).hasLength(1) + val launchRequirement = filterArgumentCaptor.lastValue.mRequirements!![0] + assertThat(launchRequirement.mModes).isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)) + assertThat(launchRequirement.mActivityType).isEqualTo(ACTIVITY_TYPE_STANDARD) + assertThat(launchRequirement.mWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(launchRequirement.mOrder).isEqualTo(CONTAINER_ORDER_TOP) + } + + @Test + @EnableFlags( + FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + FLAG_ENABLE_DESKTOP_APP_LAUNCH_BUGFIX, + ) + fun registerTransitions_usesCorrectFilter_flagEnabled() { + transitionManager.registerTransitions() + val filterArgumentCaptor = argumentCaptor() + + verify(systemUiProxy, times(1)) + .registerRemoteTransition(any(), filterArgumentCaptor.capture()) + + assertThat(filterArgumentCaptor.lastValue).isNotNull() + assertThat(filterArgumentCaptor.lastValue.mTypeSet) + .isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)) + assertThat(filterArgumentCaptor.lastValue.mRequirements).hasLength(1) + val launchRequirement = filterArgumentCaptor.lastValue.mRequirements!![0] + assertThat(launchRequirement.mModes).isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)) + assertThat(launchRequirement.mActivityType).isEqualTo(ACTIVITY_TYPE_STANDARD) + assertThat(launchRequirement.mWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(launchRequirement.mOrder).isEqualTo(CONTAINER_ORDER_ANY) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java index d4dd58040a..91f9e53eaa 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java @@ -35,10 +35,13 @@ import android.os.UserHandle; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.pm.UserCache; -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.util.AllModulesForTest; +import com.android.launcher3.util.SandboxContext; import com.android.launcher3.util.UserIconInfo; import com.android.systemui.shared.system.SysUiStatsLog; @@ -51,6 +54,9 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; +import dagger.BindsInstance; +import dagger.Component; + @SmallTest @RunWith(AndroidJUnit4.class) public class AppEventProducerTest { @@ -72,7 +78,9 @@ public class AppEventProducerTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = new SandboxContext(getApplicationContext()); - mContext.putObject(UserCache.INSTANCE, mUserCache); + mContext.initDaggerComponent( + DaggerAppEventProducerTest_TestComponent.builder().bindUserCache(mUserCache) + ); mAppEventProducer = new AppEventProducer(mContext, null); } @@ -129,4 +137,15 @@ public class AppEventProducerTest { .build()); return itemBuilder.build(); } + + @LauncherAppSingleton + @Component(modules = { AllModulesForTest.class }) + interface TestComponent extends LauncherAppComponent { + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance + AppEventProducerTest.TestComponent.Builder bindUserCache(UserCache userCache); + @Override LauncherAppComponent build(); + } + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionHelperTest.kt new file mode 100644 index 0000000000..c12d25f779 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionHelperTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model + +import android.app.prediction.AppTargetEvent +import android.content.ComponentName +import android.content.Intent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PIN_WIDGETS +import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID +import com.android.launcher3.model.PredictionHelper.BUNDLE_KEY_ADDED_APP_WIDGETS +import com.android.launcher3.model.PredictionHelper.BUNDLE_KEY_PIN_EVENTS +import com.android.launcher3.model.PredictionHelper.getBundleForHotseatPredictions +import com.android.launcher3.model.PredictionHelper.getBundleForWidgetPredictions +import com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction +import com.android.launcher3.model.PredictionHelper.isTrackedForWidgetPrediction +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.util.ModelTestExtensions.initItems +import com.android.launcher3.util.SandboxApplication +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PredictionHelperTest { + + @get:Rule val context = SandboxApplication() + private var itemIdCounter: Int = 0 + + @Test + fun isTrackedForHotseatPrediction_true_for_valid_items() { + createAppInfo("test").let { + assertTrue(it.isTrackedForHotseatPrediction()) + assertTrue(it.buildProto(context).isTrackedForHotseatPrediction()) + } + + createAppInfo("test", CONTAINER_HOTSEAT).let { + assertTrue(it.isTrackedForHotseatPrediction()) + assertTrue(it.buildProto(context).isTrackedForHotseatPrediction()) + } + } + + @Test + fun isTrackedForHotseatPrediction_false_for_invalid_items() { + createAppInfo("test", CONTAINER_DESKTOP, 3).let { + assertFalse(it.isTrackedForHotseatPrediction()) + assertFalse(it.buildProto(context).isTrackedForHotseatPrediction()) + } + + createAppInfo("test", 2 /* folder */).let { + assertFalse(it.isTrackedForHotseatPrediction()) + assertFalse(it.buildProto(context).isTrackedForHotseatPrediction()) + } + + createAppInfo("test", CONTAINER_ALL_APPS).let { + assertFalse(it.isTrackedForHotseatPrediction()) + assertFalse(it.buildProto(context).isTrackedForHotseatPrediction()) + } + } + + @Test + fun isTrackedForWidgetPrediction_true_for_valid_items() { + createWidgetInfoInfo("test").let { + assertTrue(it.isTrackedForWidgetPrediction()) + assertTrue(it.buildProto(context).isTrackedForWidgetPrediction()) + } + } + + @Test + fun isTrackedForWidgetPrediction_false_for_invalid_items() { + createAppInfo("test").let { + assertFalse(it.isTrackedForWidgetPrediction()) + assertFalse(it.buildProto(context).isTrackedForWidgetPrediction()) + } + + createWidgetInfoInfo("test", CONTAINER_PIN_WIDGETS).let { + assertFalse(it.isTrackedForWidgetPrediction()) + assertFalse(it.buildProto(context).isTrackedForWidgetPrediction()) + } + } + + @Test + fun getBundleForHotseatPredictions_contains_pin_events() { + val dataModel = BgDataModel(mock(), mock(), mock(), mock()) + dataModel.initItems( + createAppInfo("test1"), + createAppInfo("test2", CONTAINER_HOTSEAT, 2), + createAppInfo("test3", 2 /* folder */), + ) + + val items = + getBundleForHotseatPredictions(context, dataModel) + .getParcelableArrayList(BUNDLE_KEY_PIN_EVENTS)!! + assertThat(items).hasSize(2) + assertThat(items[0].launchLocation).isEqualTo("workspace/0/[0,0]/[1,1]") + assertThat(items[0].target!!.packageName).isEqualTo("test1") + + assertThat(items[1].launchLocation).isEqualTo("hotseat/2/[2,0]/[1,1]") + assertThat(items[1].target!!.packageName).isEqualTo("test2") + } + + @Test + fun getBundleForWidgetPredictions_contains_pin_events() { + val dataModel = BgDataModel(mock(), mock(), mock(), mock()) + dataModel.initItems( + createAppInfo("test1"), + createAppInfo("test2", CONTAINER_HOTSEAT, 2), + createWidgetInfoInfo("test3"), + ) + + val items = + getBundleForWidgetPredictions(context, dataModel) + .getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS)!! + assertThat(items).hasSize(1) + assertThat(items[0].launchLocation).isEqualTo("workspace/0/[0,0]/[1,1]") + assertThat(items[0].target!!.packageName).isEqualTo("test3") + } + + private fun createAppInfo( + pkg: String, + container: Int = CONTAINER_DESKTOP, + screenId: Int = FIRST_SCREEN_ID, + ) = + AppInfo().apply { + this.id = itemIdCounter + this.container = container + this.screenId = screenId + this.intent = Intent().setComponent(ComponentName(pkg, pkg)) + this.componentName = ComponentName(pkg, pkg) + this.cellX = 0 + this.cellY = 0 + + itemIdCounter++ + } + + private fun createWidgetInfoInfo( + pkg: String, + container: Int = CONTAINER_DESKTOP, + screenId: Int = FIRST_SCREEN_ID, + ) = + LauncherAppWidgetInfo().apply { + this.id = itemIdCounter + this.container = container + this.screenId = screenId + this.providerName = ComponentName(pkg, pkg) + this.cellX = 0 + this.cellY = 0 + + itemIdCounter++ + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionUpdateTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionUpdateTaskTest.kt new file mode 100644 index 0000000000..a17ab9033a --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionUpdateTaskTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model + +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetId +import android.os.Process.myUserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.icons.cache.CacheLookupFlag.Companion.DEFAULT_LOOKUP_FLAG +import com.android.launcher3.util.AllModulesForTest +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY +import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2 +import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.TestUtil +import com.google.common.truth.Truth.assertThat +import dagger.Component +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PredictionUpdateTaskTest { + + @get:Rule val context = SandboxApplication().withModelDependency() + + private val containerId = -300 + private val predictorState = PredictorState(containerId, "test-storage", DEFAULT_LOOKUP_FLAG) + + lateinit var component: TestComponent + + @Before + fun setup() { + context.initDaggerComponent(DaggerPredictionUpdateTaskTest_TestComponent.builder()) + component = context.appComponent as TestComponent + } + + @Test + fun emptyPredictions_update_data_model() { + assertThat(component.getDataModel().itemsIdMap[containerId]).isNull() + + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + PredictionUpdateTask(predictorState, listOf()) + .execute( + component.getTaskController(), + component.getDataModel(), + component.getAllAppsList(), + ) + } + assertThat(component.getDataModel().itemsIdMap[containerId]).isNotNull() + assertThat(component.getDataModel().itemsIdMap.getPredictedContents(containerId)).isEmpty() + } + + @Test + fun validPredictions_added_to_data_model() { + assertThat(component.getDataModel().itemsIdMap[containerId]).isNull() + + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + PredictionUpdateTask( + predictorState, + listOf(createAppTarget(TEST_ACTIVITY), createAppTarget(TEST_ACTIVITY2)), + ) + .execute( + component.getTaskController(), + component.getDataModel(), + component.getAllAppsList(), + ) + } + assertThat(component.getDataModel().itemsIdMap[containerId]).isNotNull() + val items = component.getDataModel().itemsIdMap.getPredictedContents(containerId) + assertThat(items).hasSize(2) + } + + private fun createAppTarget(className: String): AppTarget = + AppTarget.Builder(AppTargetId("app:$className"), TEST_PACKAGE, myUserHandle()) + .setClassName(className) + .build() + + @LauncherAppSingleton + @Component(modules = [AllModulesForTest::class]) + interface TestComponent : LauncherAppComponent { + + fun getDataModel(): BgDataModel + + fun getAllAppsList(): AllAppsList + + fun getTaskController(): ModelTaskController + + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + + override fun build(): TestComponent + } + } +} diff --git a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt similarity index 55% rename from quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt index a5327628d1..a2b0338ab4 100644 --- a/quickstep/tests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt @@ -1,3 +1,18 @@ +/* + * 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.launcher3.model import android.app.prediction.AppPredictor @@ -5,77 +20,87 @@ import android.app.prediction.AppTarget import android.app.prediction.AppTargetEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION -import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WALLPAPERS import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION -import com.android.launcher3.util.LauncherModelHelper -import org.junit.After +import com.android.launcher3.util.SandboxApplication import org.junit.Assert.assertNotSame import org.junit.Assert.assertSame import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock /** Unit tests for [QuickstepModelDelegate]. */ @RunWith(AndroidJUnit4::class) class QuickstepModelDelegateTest { + @get:Rule val context = SandboxApplication().withModelDependency() + private lateinit var underTest: QuickstepModelDelegate - private lateinit var modelHelper: LauncherModelHelper @Mock private lateinit var target: AppTarget @Mock private lateinit var mockedAppTargetEvent: AppTargetEvent @Mock private lateinit var allAppsPredictor: AppPredictor @Mock private lateinit var hotseatPredictor: AppPredictor @Mock private lateinit var widgetRecommendationPredictor: AppPredictor + @Mock private lateinit var itemParserFactory: PredictedItemFactory.Factory @Before fun setUp() { MockitoAnnotations.initMocks(this) - modelHelper = LauncherModelHelper() - underTest = QuickstepModelDelegate(modelHelper.sandboxContext) - underTest.mAllAppsState.predictor = allAppsPredictor - underTest.mHotseatState.predictor = hotseatPredictor + underTest = + QuickstepModelDelegate( + context, + context.appComponent.idp, + context.appComponent.userCache, + itemParserFactory, + "", /* dbFileName */ + ) + underTest.mAllPredictionAppsState.predictor = allAppsPredictor + underTest.mHotseatPredictionState.predictor = hotseatPredictor underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor - underTest.mApp = LauncherAppState.getInstance(modelHelper.sandboxContext) - underTest.mDataModel = BgDataModel() - } - - @After - fun tearDown() { - modelHelper.destroy() + underTest.mModel = LauncherAppState.getInstance(context).model + underTest.mDataModel = + BgDataModel( + WidgetsModel(context), + /* homeDataProvider */ { null }, + /* dumpManager */ mock(), + /* DaggerSingletonTracker */ mock(), + ) } @Test fun onAppTargetEvent_notifyTarget() { - underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_PREDICTION) + underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_ALL_APPS_PREDICTION) verify(allAppsPredictor).notifyAppTargetEvent(mockedAppTargetEvent) - verifyZeroInteractions(hotseatPredictor) - verifyZeroInteractions(widgetRecommendationPredictor) + verifyNoMoreInteractions(hotseatPredictor) + verifyNoMoreInteractions(widgetRecommendationPredictor) } @Test fun onWidgetPrediction_notifyWidgetRecommendationPredictor() { underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WIDGETS_PREDICTION) - verifyZeroInteractions(allAppsPredictor) + verifyNoMoreInteractions(allAppsPredictor) verify(widgetRecommendationPredictor).notifyAppTargetEvent(mockedAppTargetEvent) - verifyZeroInteractions(hotseatPredictor) + verifyNoMoreInteractions(hotseatPredictor) } @Test fun onHotseatPrediction_notifyHotseatPredictor() { underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_HOTSEAT_PREDICTION) - verifyZeroInteractions(allAppsPredictor) - verifyZeroInteractions(widgetRecommendationPredictor) + verifyNoMoreInteractions(allAppsPredictor) + verifyNoMoreInteractions(widgetRecommendationPredictor) verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent) } @@ -83,45 +108,45 @@ class QuickstepModelDelegateTest { fun onOtherClient_notifyHotseatPredictor() { underTest.onAppTargetEvent(mockedAppTargetEvent, CONTAINER_WALLPAPERS) - verifyZeroInteractions(allAppsPredictor) - verifyZeroInteractions(widgetRecommendationPredictor) + verifyNoMoreInteractions(allAppsPredictor) + verifyNoMoreInteractions(widgetRecommendationPredictor) verify(hotseatPredictor).notifyAppTargetEvent(mockedAppTargetEvent) } @Test fun hotseatActionPin_recreateHotSeat() { - assertSame(underTest.mHotseatState.predictor, hotseatPredictor) + assertSame(underTest.mHotseatPredictionState.predictor, hotseatPredictor) val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_PIN).build() underTest.markActive() underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION) verify(hotseatPredictor).destroy() - assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor) + assertNotSame(underTest.mHotseatPredictionState.predictor, hotseatPredictor) } @Test fun hotseatActionUnpin_recreateHotSeat() { - assertSame(underTest.mHotseatState.predictor, hotseatPredictor) + assertSame(underTest.mHotseatPredictionState.predictor, hotseatPredictor) underTest.markActive() val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build() underTest.onAppTargetEvent(appTargetEvent, CONTAINER_HOTSEAT_PREDICTION) verify(hotseatPredictor).destroy() - assertNotSame(underTest.mHotseatState.predictor, hotseatPredictor) + assertNotSame(underTest.mHotseatPredictionState.predictor, hotseatPredictor) } @Test fun container_actionPin_notRecreateHotSeat() { - assertSame(underTest.mHotseatState.predictor, hotseatPredictor) + assertSame(underTest.mHotseatPredictionState.predictor, hotseatPredictor) val appTargetEvent = AppTargetEvent.Builder(target, AppTargetEvent.ACTION_UNPIN).build() underTest.markActive() - underTest.onAppTargetEvent(appTargetEvent, CONTAINER_PREDICTION) + underTest.onAppTargetEvent(appTargetEvent, CONTAINER_ALL_APPS_PREDICTION) verify(allAppsPredictor, never()).destroy() verify(hotseatPredictor, never()).destroy() - assertSame(underTest.mHotseatState.predictor, hotseatPredictor) + assertSame(underTest.mHotseatPredictionState.predictor, hotseatPredictor) } } diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt similarity index 64% rename from quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt index 5c7b4aba4d..d445189039 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt @@ -16,6 +16,8 @@ package com.android.launcher3.model +import android.app.prediction.AppPredictionManager +import android.app.prediction.AppPredictor import android.app.prediction.AppTarget import android.app.prediction.AppTargetEvent import android.app.prediction.AppTargetId @@ -30,14 +32,21 @@ import com.android.launcher3.DeviceProfile import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherAppState import com.android.launcher3.icons.IconCache +import com.android.launcher3.model.WidgetPredictionsRequester.LAUNCH_LOCATION import com.android.launcher3.model.WidgetPredictionsRequester.buildBundleForPredictionSession import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter import com.android.launcher3.util.ActivityContextWrapper -import com.android.launcher3.util.PackageUserKey +import com.android.launcher3.util.ComponentKey +import com.android.launcher3.util.Executors +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.TestUtil import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.android.launcher3.widget.PendingAddWidgetInfo import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import java.util.function.Predicate import junit.framework.Assert.assertNotNull import org.junit.Before @@ -45,6 +54,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class WidgetsPredictionsRequesterTest { @@ -62,15 +74,30 @@ class WidgetsPredictionsRequesterTest { private lateinit var widgetItem1b: WidgetItem private lateinit var widgetItem2: WidgetItem - private lateinit var allWidgets: Map> + private lateinit var allWidgets: Map @Mock private lateinit var iconCache: IconCache + @Mock private lateinit var apmMock: AppPredictionManager + + @Mock private lateinit var predictorMock: AppPredictor + @Before fun setUp() { MockitoAnnotations.initMocks(this) mUserHandle = myUserHandle() - context = ActivityContextWrapper(ApplicationProvider.getApplicationContext()) + + whenever(apmMock.createAppPredictionSession(any())).thenReturn(predictorMock) + + context = + object : ActivityContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getSystemService(name: String): Any? { + if (name == "app_prediction") { + return apmMock + } + return super.getSystemService(name) + } + } testInvariantProfile = LauncherAppState.getIDP(context) deviceProfile = testInvariantProfile.getDeviceProfile(context).copy(context) @@ -93,9 +120,9 @@ class WidgetsPredictionsRequesterTest { allWidgets = mapOf( - PackageUserKey(APP_1_PACKAGE_NAME, mUserHandle) to - listOf(widgetItem1a, widgetItem1b), - PackageUserKey(APP_2_PACKAGE_NAME, mUserHandle) to listOf(widgetItem2), + ComponentKey(widgetItem1a.componentName, widgetItem1a.user) to widgetItem1a, + ComponentKey(widgetItem1b.componentName, widgetItem1b.user) to widgetItem1b, + ComponentKey(widgetItem2.componentName, widgetItem2.user) to widgetItem2, ) } @@ -103,7 +130,7 @@ class WidgetsPredictionsRequesterTest { fun buildBundleForPredictionSession_includesAddedAppWidgets() { val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo, widget2Info) - val bundle = buildBundleForPredictionSession(existingWidgets, TEST_UI_SURFACE) + val bundle = buildBundleForPredictionSession(existingWidgets) val addedWidgetsBundleExtra = bundle.getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, AppTarget::class.java) @@ -113,21 +140,67 @@ class WidgetsPredictionsRequesterTest { buildExpectedAppTargetEvent( /*pkg=*/ APP_1_PACKAGE_NAME, /*providerClassName=*/ APP_1_PROVIDER_A_CLASS_NAME, - /*user=*/ mUserHandle + /*user=*/ mUserHandle, ), buildExpectedAppTargetEvent( /*pkg=*/ APP_1_PACKAGE_NAME, /*providerClassName=*/ APP_1_PROVIDER_B_CLASS_NAME, - /*user=*/ mUserHandle + /*user=*/ mUserHandle, ), buildExpectedAppTargetEvent( /*pkg=*/ APP_2_PACKAGE_NAME, /*providerClassName=*/ APP_2_PROVIDER_1_CLASS_NAME, - /*user=*/ mUserHandle - ) + /*user=*/ mUserHandle, + ), ) } + @Test + fun request_invokesCallbackWithPredictedItems() { + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + val underTest = WidgetPredictionsRequester(context, TEST_UI_SURFACE, allWidgets) + val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo) + val predictions = + listOf( + // (existing) already on surface + AppTarget( + AppTargetId(APP_1_PACKAGE_NAME), + APP_1_PACKAGE_NAME, + APP_1_PROVIDER_B_CLASS_NAME, + mUserHandle, + ), + // eligible + AppTarget( + AppTargetId(APP_2_PACKAGE_NAME), + APP_2_PACKAGE_NAME, + APP_2_PROVIDER_1_CLASS_NAME, + mUserHandle, + ), + ) + doAnswer { + underTest.onTargetsAvailable(predictions) + null + } + .whenever(predictorMock) + .requestPredictionUpdate() + val testCountDownLatch = CountDownLatch(1) + val listener = + WidgetPredictionsRequester.WidgetPredictionsListener { itemInfos -> + if (itemInfos.size == 1 && itemInfos[0] is PendingAddWidgetInfo) { + // only one item was eligible. + testCountDownLatch.countDown() + } else { + println("Unexpected prediction items found: ${itemInfos.size}") + } + } + + underTest.request(existingWidgets, listener) + TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {} + + assertThat(testCountDownLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue() + } + } + @Test fun filterPredictions_notOnUiSurfaceFilter_returnsOnlyEligiblePredictions() { val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo) @@ -140,15 +213,15 @@ class WidgetsPredictionsRequesterTest { AppTargetId(APP_1_PACKAGE_NAME), APP_1_PACKAGE_NAME, APP_1_PROVIDER_B_CLASS_NAME, - mUserHandle + mUserHandle, ), // eligible AppTarget( AppTargetId(APP_2_PACKAGE_NAME), APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME, - mUserHandle - ) + mUserHandle, + ), ) // only 2 was eligible @@ -156,7 +229,7 @@ class WidgetsPredictionsRequesterTest { } @Test - fun filterPredictions_appPredictions_returnsWidgetFromPackage() { + fun filterPredictions_appPredictions_returnsEmptyList() { val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo) val filter: Predicate = notOnUiSurfaceFilter(widgetsAlreadyOnSurface) @@ -166,28 +239,27 @@ class WidgetsPredictionsRequesterTest { AppTargetId(APP_1_PACKAGE_NAME), APP_1_PACKAGE_NAME, "$APP_1_PACKAGE_NAME.SomeActivity", - mUserHandle + mUserHandle, ), AppTarget( AppTargetId(APP_2_PACKAGE_NAME), APP_2_PACKAGE_NAME, "$APP_2_PACKAGE_NAME.SomeActivity2", - mUserHandle + mUserHandle, ), ) - assertThat(filterPredictions(predictions, allWidgets, filter)) - .containsExactly(widgetItem1a, widgetItem2) + assertThat(filterPredictions(predictions, allWidgets, filter)).isEmpty() } - private fun createWidgetItem( - providerInfo: AppWidgetProviderInfo, - ): WidgetItem { + private fun createWidgetItem(providerInfo: AppWidgetProviderInfo): WidgetItem { val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo) return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context) } companion object { + const val TEST_TIMEOUT = 3L + const val TEST_UI_SURFACE = "widgets_test" const val BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets" @@ -203,18 +275,18 @@ class WidgetsPredictionsRequesterTest { private fun buildExpectedAppTargetEvent( pkg: String, providerClassName: String, - userHandle: UserHandle + userHandle: UserHandle, ): AppTargetEvent { val appTarget = AppTarget.Builder( /*id=*/ AppTargetId("widget:$pkg"), /*packageName=*/ pkg, - /*user=*/ userHandle + /*user=*/ userHandle, ) .setClassName(providerClassName) .build() return AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) - .setLaunchLocation(TEST_UI_SURFACE) + .setLaunchLocation(LAUNCH_LOCATION) .build() } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt new file mode 100644 index 0000000000..61c0a1b54b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt @@ -0,0 +1,233 @@ +/* + * 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.launcher3.model.data + +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE +import com.android.launcher3.model.data.TaskViewItemInfo.Companion.createTaskViewAtom +import com.android.launcher3.pm.UserCache +import com.android.launcher3.util.AllModulesForTest +import com.android.launcher3.util.SandboxContext +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TestDispatcherProvider +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.launcher3.util.UserIconInfo +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.task.thumbnail.TaskContentView +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import kotlinx.coroutines.test.StandardTestDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Test for [TaskViewItemInfo] */ +@RunWith(AndroidJUnit4::class) +class TaskViewItemInfoTest { + private val context = SandboxContext(InstrumentationRegistry.getInstrumentation().targetContext) + private val taskView = mock() + private val recentsView = mock>() + private val overlayFactory = mock() + private val userCache = mock() + private val userInfo = mock() + private val dispatcher = StandardTestDispatcher() + + @Before + fun setUp() { + whenever(overlayFactory.createOverlay(any())).thenReturn(mock>()) + whenever(taskView.context).thenReturn(context) + whenever(taskView.recentsView).thenReturn(recentsView) + whenever(recentsView.indexOfChild(taskView)).thenReturn(TASK_VIEW_INDEX) + whenever(userInfo.isPrivate).thenReturn(false) + whenever(userCache.getUserInfo(any())).thenReturn(userInfo) + context.initDaggerComponent( + DaggerTaskViewItemInfoTest_TestComponent.builder().bindUserCache(userCache) + ) + RecentsDependencies.maybeInitialize(context, TestDispatcherProvider(dispatcher)) + } + + @After + fun tearDown() { + RecentsDependencies.destroy(context) + } + + @Test + fun singleTask() { + val taskContainers = listOf(createTaskContainer(createTask(1))) + whenever(taskView.type).thenReturn(TaskViewType.SINGLE) + whenever(taskView.taskContainers).thenReturn(taskContainers) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 0, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 1, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0) + } + + @Test + fun splitTask() { + val taskContainers = + listOf(createTaskContainer(createTask(1)), createTaskContainer(createTask(2))) + whenever(taskView.type).thenReturn(TaskViewType.GROUPED) + whenever(taskView.taskContainers).thenReturn(taskContainers) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 1, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 2, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0) + } + + @Test + fun desktopTask() { + val taskContainers = + listOf( + createTaskContainer(createTask(1)), + createTaskContainer(createTask(2)), + createTaskContainer(createTask(3)), + ) + whenever(taskView.type).thenReturn(TaskViewType.DESKTOP) + whenever(taskView.taskContainers).thenReturn(taskContainers) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 2, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 3, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE).isEqualTo(0) + } + + @Test + fun privateTask() { + val taskContainers = listOf(createTaskContainer(createTask(1))) + whenever(taskView.type).thenReturn(TaskViewType.SINGLE) + whenever(taskView.taskContainers).thenReturn(taskContainers) + whenever(userInfo.isPrivate).thenReturn(true) + + val taskViewItemInfo = TaskViewItemInfo(taskContainers[0].taskView, taskContainers[0]) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 0, + index = TASK_VIEW_INDEX, + componentName = "${PACKAGE}/${CLASS}", + cardinality = 1, + ) + ) + assertThat(taskViewItemInfo.runtimeStatusFlags and FLAG_NOT_PINNABLE) + .isEqualTo(FLAG_NOT_PINNABLE) + } + + @Test + fun emptyDesktopTask() { + whenever(taskView.type).thenReturn(TaskViewType.DESKTOP) + + val taskViewItemInfo = TaskViewItemInfo(taskView = taskView, taskContainer = null) + + assertThat(taskViewItemInfo.taskViewAtom) + .isEqualTo( + createTaskViewAtom( + type = 2, + index = TASK_VIEW_INDEX, + componentName = "", + cardinality = 0, + ) + ) + assertThat(taskViewItemInfo.user).isEqualTo(Process.myUserHandle()) + assertThat(taskViewItemInfo.intent).isNotNull() + } + + private fun createTask(id: Int) = + Task(TaskKey(id, 0, Intent(), ComponentName(PACKAGE, CLASS), 0, 2000)) + + private fun createTaskContainer(task: Task): TaskContainer { + return TaskContainer( + taskView, + task, + mock(), + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + overlayFactory, + ) + } + + @LauncherAppSingleton + @Component(modules = [AllModulesForTest::class]) + interface TestComponent : LauncherAppComponent { + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindUserCache(userCache: UserCache): Builder + + override fun build(): TestComponent + } + } + + companion object { + const val PACKAGE = "package" + const val CLASS = "class" + const val TASK_VIEW_INDEX = 4 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt new file mode 100644 index 0000000000..180a0fb15c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.statehandlers + +import android.content.Context +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.quickstep.SystemUiProxy +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import org.junit.Assert.assertFalse +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Tests the behavior of [DesktopVisibilityController] in regards to multiple desktops and multiple + * displays. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DesktopVisibilityControllerTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val context = mock() + private val systemUiProxy = mock() + private val lifeCycleTracker = mock() + private lateinit var desktopVisibilityController: DesktopVisibilityController + + @Before + fun setUp() { + whenever(context.resources).thenReturn(mock()) + whenever(DesktopModeStatus.enableMultipleDesktops(context)).thenReturn(true) + desktopVisibilityController = + DesktopVisibilityController(context, systemUiProxy, lifeCycleTracker) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND) + fun noCrashWhenCheckingNonExistentDisplay() { + assertFalse(desktopVisibilityController.isInDesktopMode(displayId = 500)) + assertFalse(desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId = 300)) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt similarity index 97% rename from quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt index 04012c027d..b39c3f1e35 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt @@ -17,7 +17,7 @@ package com.android.launcher3.taskbar -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.statemanager.StateManager import com.android.quickstep.RecentsActivity import com.android.quickstep.fallback.RecentsState @@ -33,7 +33,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() { - lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController + lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController lateinit var stateListener: StateManager.StateListener private val recentsActivity: RecentsActivity = mock() diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/KeyboardQuickSwitchControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/KeyboardQuickSwitchControllerTest.kt new file mode 100644 index 0000000000..922dee95d5 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/KeyboardQuickSwitchControllerTest.kt @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Rect +import android.os.Process +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import android.window.RemoteTransition +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING +import com.android.launcher3.Flags.FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.rules.AllTaskbarSandboxModules +import com.android.launcher3.taskbar.rules.MockedRecentsModelHelper +import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule +import com.android.launcher3.taskbar.rules.SandboxParams +import com.android.launcher3.taskbar.rules.TaskbarSandboxComponent +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.TestUtil.getOnUiThread +import com.android.quickstep.RecentsModel +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SlideInRemoteTransition +import com.android.quickstep.util.SplitTask +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.desktopmode.IDesktopTaskListener +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +@Ignore("b/413540825") +class KeyboardQuickSwitchControllerTest { + private var systemUiProxySpy: SystemUiProxy? = null + private var desktopTaskListener: IDesktopTaskListener? = null + private val mockRecentsModelHelper: MockedRecentsModelHelper = MockedRecentsModelHelper() + private val taskIdCaptor = argumentCaptor() + private val transitionCaptor = argumentCaptor() + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) + val context = + TaskbarWindowSandboxContext.create( + SandboxParams( + { + spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy -> + systemUiProxySpy = proxy + doAnswer { desktopTaskListener = it.getArgument(0) } + .whenever(proxy) + .setDesktopTaskListener(anyOrNull()) + } + }, + builderBase = + DaggerKeyboardQuickSwitchControllerComponent.builder() + .bindRecentsModel(mockRecentsModelHelper.mockRecentsModel), + ) + ) + + @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(mockRecentsModelHelper) + @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController + + private val isKqsShown: Boolean + get() = getOnUiThread { keyboardQuickSwitchController.isShown } + + private val shownTaskIds: List + get() = getOnUiThread { keyboardQuickSwitchController.shownTaskIds() } + + @Test + fun noRecentTasks_noShownTaskIds() { + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds).isEmpty() + } + + @Test + fun onlySingleTasksPresent_shouldShowAllTaskIds() { + updateRecentsModel( + listOf(createSingleTask(PREVIOUS_TASK_ID), createSingleTask(RUNNING_TASK_ID)) + ) + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID).inOrder() + } + + @Test + fun onlyDesktopTasksPresent_shouldShowAllTaskIds() { + updateRecentsModel(listOf(createDesktopTask(listOf(RUNNING_TASK_ID, PREVIOUS_TASK_ID)))) + enableDesktopMode() + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID).inOrder() + } + + @Test + @DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + fun singleAndDesktopTasksPresent_notOnDesktopWithFlatenningOff_onlyShowSingleTaskIds() { + updateRecentsModel( + listOf( + createDesktopTask(listOf(PREVIOUS_TASK_ID, OLDEST_TASK_ID)), + createSingleTask(RUNNING_TASK_ID), + ) + ) + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID) + } + + @Test + @DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + fun singleAndDesktopTasksPresent_onDesktopWithFlatenningOff_onlyShowDesktopTaskIds() { + updateRecentsModel( + listOf( + createDesktopTask(listOf(RUNNING_TASK_ID, OLDEST_TASK_ID)), + createSingleTask(PREVIOUS_TASK_ID), + ) + ) + enableDesktopMode() + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, OLDEST_TASK_ID).inOrder() + } + + @Test + @EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + fun singleAndDesktopTasksPresent_onDesktopWithFlatenningOn_showAllTaskIds() { + updateRecentsModel( + listOf( + createDesktopTask(listOf(RUNNING_TASK_ID, OLDEST_TASK_ID)), + createSingleTask(PREVIOUS_TASK_ID), + ) + ) + enableDesktopMode() + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds) + .containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID, OLDEST_TASK_ID) + .inOrder() + } + + @Test + @EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + fun singleAndDesktopTasksPresent_notOnDesktopWithFlatenningOn_showAllTaskIds() { + updateRecentsModel( + listOf( + createDesktopTask(listOf(PREVIOUS_TASK_ID, OLDEST_TASK_ID)), + createSingleTask(RUNNING_TASK_ID), + ) + ) + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds) + .containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID, OLDEST_TASK_ID) + .inOrder() + } + + @Test + @DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING, FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS) + fun multipleDesktopTasksPresent_onDesktopWithCdFlagOff_onlyShowCurrentDesktopTasks() { + updateRecentsModel( + listOf( + createDesktopTask(listOf(RUNNING_TASK_ID)), + createDesktopTask(listOf(PREVIOUS_TASK_ID)), + ) + ) + enableDesktopMode() + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID) + } + + @Test + @DisableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + @EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_ON_CONNECTED_DISPLAYS) + fun multipleDesktopTasksPresent_onDesktopWithCdFlagON_showAllDesktopTasks() { + updateRecentsModel( + listOf( + createDesktopTask(listOf(RUNNING_TASK_ID)), + createDesktopTask(listOf(PREVIOUS_TASK_ID)), + ) + ) + enableDesktopMode() + + triggerAltTab() + + assertThat(isKqsShown).isTrue() + assertThat(shownTaskIds).containsExactly(RUNNING_TASK_ID, PREVIOUS_TASK_ID).inOrder() + } + + @Test + @EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + fun splitAndSingleTaskPresent_withFlatenningOn_shouldSortTaskIds() { + updateRecentsModel( + listOf( + createSplitTask(OLDEST_TASK_ID to RUNNING_TASK_ID), + createSingleTask(PREVIOUS_TASK_ID), + ) + ) + + triggerAltTab() + + // Although single task is more recent than one of the split tasks, the split tasks should + // be together. Furthermore, the shownTaskIds returns left split task first. + assertThat(shownTaskIds) + .containsExactly(OLDEST_TASK_ID, RUNNING_TASK_ID, PREVIOUS_TASK_ID) + .inOrder() + } + + @Test + @EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + fun launchDesktopApp_notOnDesktop_shouldCallSysUIProxyToStartSpecificApp() { + val deskId = 1 + updateRecentsModel(listOf(createDesktopTask(listOf(PREVIOUS_TASK_ID), deskId))) + + triggerAltTabAndLaunchFocusedTask() + + val deskIdCaptor = argumentCaptor() + verify(systemUiProxySpy)?.activateDesk(deskIdCaptor.capture(), transitionCaptor.capture()) + assertThat(deskIdCaptor.firstValue).isEqualTo(deskId) + assertThat(transitionCaptor.firstValue.remoteTransition) + .isInstanceOf(SlideInRemoteTransition::class.java) + + verify(systemUiProxySpy) + ?.showDesktopApp(taskIdCaptor.capture(), eq(null), eq(DesktopTaskToFrontReason.ALT_TAB)) + assertThat(taskIdCaptor.firstValue).isEqualTo(PREVIOUS_TASK_ID) + } + + @Test + @EnableFlags(FLAG_ENABLE_ALT_TAB_KQS_FLATENNING) + fun launchSingleApp_onDesktop_shouldCallSysUIProxyToMoveToFullscreen() { + updateRecentsModel(listOf(createSingleTask(PREVIOUS_TASK_ID))) + enableDesktopMode() + + triggerAltTabAndLaunchFocusedTask() + + verify(systemUiProxySpy) + ?.moveToFullscreen( + taskIdCaptor.capture(), + eq(DesktopModeTransitionSource.KEYBOARD_SHORTCUT), + transitionCaptor.capture(), + ) + assertThat(taskIdCaptor.firstValue).isEqualTo(PREVIOUS_TASK_ID) + assertThat(transitionCaptor.firstValue.remoteTransition) + .isInstanceOf(SlideInRemoteTransition::class.java) + } + + private fun createSingleTask(taskId: Int) = SingleTask(createTask(taskId)) + + private fun createSplitTask(taskIds: Pair) = + SplitTask( + createTask(taskIds.first), + createTask(taskIds.second), + SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ 1, + /* rightBottomTaskId = */ 2, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50, + ), + ) + + private fun createDesktopTask(taskIds: List, deskId: Int = 0) = + DesktopTask(deskId, DEFAULT_DISPLAY, taskIds.map { createTask(it) }) + + private fun enableDesktopMode() { + whenever(DesktopVisibilityController.INSTANCE[context].isInDesktopMode(any())) + .thenReturn(true) + } + + /* + * Returns a task with the given ID and a fake package name. + * + * Note: the task ID is added to last active time, thus higher task ID indicates a more recent + * active task. + */ + private fun createTask(taskId: Int): Task { + return Task( + Task.TaskKey( + taskId, + 0, + Intent().apply { `package` = "Fake${taskId}" }, + ComponentName("Fake${taskId}", ""), + Process.myUserHandle().identifier, + 2000L + taskId, + ) + ) + } + + private fun updateRecentsModel(tasks: List) { + recentsModel.updateRecentTasks(tasks) + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + } + + private fun triggerAltTab() = runOnMainSync { + keyboardQuickSwitchController.openQuickSwitchView() + recentsModel.resolvePendingTaskRequests() + } + + private fun triggerAltTabAndLaunchFocusedTask() { + triggerAltTab() + runOnMainSync { keyboardQuickSwitchController.launchFocusedTask() } + } + + private companion object { + const val OLDEST_TASK_ID = 1 + const val PREVIOUS_TASK_ID = 2 + const val RUNNING_TASK_ID = 3 + } +} + +/** KeyboardQuickSwitchControllerComponent used to bind the RecentsModel. */ +@LauncherAppSingleton +@Component(modules = [AllTaskbarSandboxModules::class]) +interface KeyboardQuickSwitchControllerComponent : TaskbarSandboxComponent { + + @Component.Builder + interface Builder : TaskbarSandboxComponent.Builder { + @BindsInstance fun bindRecentsModel(model: RecentsModel): Builder + + override fun build(): KeyboardQuickSwitchControllerComponent + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt new file mode 100644 index 0000000000..50d6affe4b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt @@ -0,0 +1,109 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING +import com.android.launcher3.taskbar.rules.SandboxParams +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.quickstep.SystemUiProxy +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +class TaskbarAutohideSuspendControllerTest { + + @get:Rule(order = 0) + val context = + TaskbarWindowSandboxContext.create( + SandboxParams({ + spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy -> + doAnswer { latestSuspendNotification = it.getArgument(0) } + .whenever(proxy) + .notifyTaskbarAutohideSuspend(anyOrNull()) + } + }) + ) + @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController + @InjectController lateinit var stashController: TaskbarStashController + + private var latestSuspendNotification: Boolean? = null + + @Test + fun testUpdateFlag_suspendInLauncher_notifiesSuspend() { + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, true) + } + assertThat(latestSuspendNotification).isTrue() + } + + @Test + fun testUpdateFlag_toggleSuspendDraggingTwice_notifiesUnsuspend() { + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, true) + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, false) + } + assertThat(latestSuspendNotification).isFalse() + } + + @Test + fun testUpdateFlag_resetsAlreadyUnsetFlag_noNotifyUnsuspend() { + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, false) + } + assertThat(latestSuspendNotification).isNull() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateFlag_suspendTransientTaskbarForTouch_cancelsAutoStashTimeout() { + // Unstash and verify alarm. + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + + // EDU opens while unstashed. + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_TOUCHING, true) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isFalse() + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt similarity index 93% rename from quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt index 15b1e532bd..bfd5d76923 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt @@ -17,6 +17,7 @@ package com.android.launcher3.taskbar import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController import com.android.launcher3.taskbar.bubbles.BubbleControllers +import com.android.launcher3.taskbar.growth.NudgeController import com.android.launcher3.taskbar.overlay.TaskbarOverlayController import com.android.systemui.shared.rotation.RotationButtonController import java.util.Optional @@ -57,6 +58,9 @@ abstract class TaskbarBaseTestCase { @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController @Mock lateinit var taskbarPinningController: TaskbarPinningController @Mock lateinit var optionalBubbleControllers: Optional + @Mock lateinit var taskbarDesktopModeController: TaskbarDesktopModeController + @Mock lateinit var nudgeController: NudgeController + @Mock lateinit var nudgeViewController: NudgeViewController lateinit var taskbarControllers: TaskbarControllers @@ -98,6 +102,9 @@ abstract class TaskbarBaseTestCase { keyboardQuickSwitchController, taskbarPinningController, optionalBubbleControllers, + taskbarDesktopModeController, + nudgeController, + nudgeViewController ) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt new file mode 100644 index 0000000000..0e066cd13b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt @@ -0,0 +1,50 @@ +/* + * 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.launcher3.taskbar + +import android.content.Context +import com.android.launcher3.ConstantItem +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.TestUtil +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +object TaskbarControllerTestUtil { + inline fun runOnMainSync(crossinline runTest: () -> Unit) { + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) { runTest() } + } + + /** Returns a property to read/write the value of a [ConstantItem]. */ + fun ConstantItem.asProperty(context: Context): ReadWriteProperty { + return TaskbarItemProperty(context, this) + } + + private class TaskbarItemProperty( + private val context: Context, + private val item: ConstantItem, + ) : ReadWriteProperty { + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return LauncherPrefs.get(context).get(item) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + runOnMainSync { LauncherPrefs.get(context).put(item, value) } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt new file mode 100644 index 0000000000..455b6c5b98 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt @@ -0,0 +1,48 @@ +/* + * 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.launcher3.taskbar + +import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarDesktopModeControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @TaskbarUnitTestRule.InjectController + lateinit var taskbarDesktopModeController: TaskbarDesktopModeController + + @Test + fun whenTaskbarRequiresCornerRoundness_shouldReturnDefaultCornerRoundness() { + assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(true)) + .isEqualTo(MAX_ROUNDNESS) + } + + @Test + fun whenTaskbarRequiresCornerRoundness_shouldReturnZeroAsCornerRoundness() { + assertThat(taskbarDesktopModeController.getTaskbarCornerRoundness(false)).isEqualTo(0f) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt new file mode 100644 index 0000000000..3c80352329 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt @@ -0,0 +1,187 @@ +/* + * 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.launcher3.taskbar + +import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.OnboardingPrefs +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarEduTooltipControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + + @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) + + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private val wasInTestHarness = Utilities.isRunningInTestHarness() + + private var tooltipStep by OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem.asProperty(context) + private var searchEduSeen by OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN.asProperty(context) + + @Before + fun setUp() { + Utilities.disableRunningInTestHarnessForTests() + } + + @After + fun tearDown() { + if (wasInTestHarness) { + Utilities.enableRunningInTestHarnessForTests() + } + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testMaybeShowSwipeEdu_whenTaskbarIsInThreeButtonMode_doesNotShowSwipeEdu() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowSwipeEdu_whenSwipeEduAlreadyShown_doesNotShowSwipeEdu() { + tooltipStep = TOOLTIP_STEP_FEATURES + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowSwipeEdu_whenUserHasNotSeen_doesShowSwipeEdu() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowFeaturesEdu_whenFeatureEduAlreadyShown_doesNotShowFeatureEdu() { + tooltipStep = TOOLTIP_STEP_NONE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowFeaturesEdu_whenUserHasNotSeen_doesShowFeatureEdu() { + tooltipStep = TOOLTIP_STEP_FEATURES + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testMaybeShowPinningEdu_whenTaskbarIsInThreeButtonMode_doesNotShowPinningEdu() { + tooltipStep = TOOLTIP_STEP_PINNING + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowPinningEdu_whenUserHasNotSeen_doesShowPinningEdu() { + // Test standalone pinning edu, where user has seen taskbar edu before, but not pinning edu. + tooltipStep = TOOLTIP_STEP_PINNING + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING) + runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() } + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsBeforeTooltipFeaturesStep_whenUserHasNotSeenFeatureEdu_shouldReturnTrue() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsBeforeTooltipFeaturesStep_whenUserHasSeenFeatureEdu_shouldReturnFalse() { + tooltipStep = TOOLTIP_STEP_NONE + assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testHide_whenTooltipIsOpen_shouldCloseTooltip() { + tooltipStep = TOOLTIP_STEP_SWIPE + assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE) + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() } + assertThat(taskbarEduTooltipController.isTooltipOpen).isTrue() + runOnMainSync { taskbarEduTooltipController.hide() } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testMaybeShowSearchEdu_whenTaskbarIsTransient_shouldNotShowSearchEdu() { + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + runOnMainSync { taskbarEduTooltipController.init(taskbarContext.controllers) } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testMaybeShowSearchEdu_whenTaskbarIsPinnedAndUserHasSeenSearchEdu_shouldNotShowSearchEdu() { + searchEduSeen = true + assertThat(taskbarEduTooltipController.userHasSeenSearchEdu).isTrue() + runOnMainSync { taskbarEduTooltipController.hide() } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + runOnMainSync { taskbarEduTooltipController.init(taskbarContext.controllers) } + assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt new file mode 100644 index 0000000000..529174a253 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_HOVER_ENTER +import android.view.MotionEvent.ACTION_HOVER_EXIT +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.BubbleTextView +import com.android.launcher3.R +import com.android.launcher3.apppairs.AppPairIcon +import com.android.launcher3.folder.FolderIcon +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatAppPairsItem +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatFolderItem +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatWorkspaceItem +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarHoverToolTipControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController + @InjectController lateinit var popupController: TaskbarPopupController + + private val taskbarContext: TaskbarActivityContext by taskbarUnitTestRule::activityContext + + private lateinit var taskbarView: TaskbarView + private lateinit var iconView: BubbleTextView + private lateinit var appPairIcon: AppPairIcon + private lateinit var folderIcon: FolderIcon + + private val isHoverToolTipOpen: Boolean + get() { + // TaskbarHoverToolTip uses ArrowTipView which is type TYPE_ON_BOARD_POPUP. + return AbstractFloatingView.hasOpenView( + taskbarContext, + AbstractFloatingView.TYPE_ON_BOARD_POPUP, + ) + } + + @Before + fun setup() { + runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) } + + val hotseatItems = + arrayOf( + createHotseatWorkspaceItem(), + createHotseatAppPairsItem(), + createHotseatFolderItem(), + ) + runOnMainSync { + taskbarView.updateItems(hotseatItems, emptyList()) + iconView = + taskbarView.iconViews.filterIsInstance().first { + it.tag is WorkspaceItemInfo + } + appPairIcon = taskbarView.iconViews.filterIsInstance().first() + folderIcon = taskbarView.iconViews.filterIsInstance().first() + } + } + + @Test + fun onHover_hoverEnterIcon_revealToolTip_hoverExitIcon_closeToolTip() { + runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isTrue() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue() + runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_EXIT) } + assertThat(isHoverToolTipOpen).isFalse() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse() + } + + @Test + fun onHover_hoverEnterFolderIcon_revealToolTip_hoverExitFolderIcon_closeToolTip() { + runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isTrue() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue() + runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_EXIT) } + assertThat(isHoverToolTipOpen).isFalse() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse() + } + + @Test + fun onHover_hoverEnterAppPair_revealToolTip_hoverExitAppPair_closeToolTip() { + runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isTrue() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue() + runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_EXIT) } + assertThat(isHoverToolTipOpen).isFalse() + assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse() + } + + @Test + fun onHover_hoverEnterIconAlignedWithHotseat_noToolTip() { + taskbarContext.setUIController( + object : TaskbarUIController() { + override fun isIconAlignedWithHotseat(): Boolean = true + } + ) + + runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) } + assertThat(isHoverToolTipOpen).isFalse() + } + + @Test + fun onHover_hoverEnterFolderOpen_noToolTip() { + runOnMainSync { + folderIcon.folder.animateOpen() + iconView.dispatchGenericMotionEvent(HOVER_ENTER) + } + assertThat(isHoverToolTipOpen).isFalse() + } + + @Test + fun onHover_hoverEnterPopupOpen_noToolTip() { + runOnMainSync { + popupController.showForIcon(iconView) + iconView.dispatchGenericMotionEvent(HOVER_ENTER) + } + assertThat(isHoverToolTipOpen).isFalse() + } + + @Test + fun onHover_emptyTitle_noTooltip() { + runOnMainSync { + iconView.text = "" + iconView.dispatchGenericMotionEvent(HOVER_ENTER) + } + assertThat(isHoverToolTipOpen).isFalse() + } + + companion object { + private val HOVER_EXIT = MotionEvent.obtain(0, 0, ACTION_HOVER_EXIT, 0f, 0f, 0) + private val HOVER_ENTER = MotionEvent.obtain(0, 0, ACTION_HOVER_ENTER, 0f, 0f, 0) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarInsetsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarInsetsControllerTest.kt new file mode 100644 index 0000000000..69c56f4e34 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarInsetsControllerTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.animation.AnimatorTestRule +import android.view.ViewTreeObserver +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.DEFAULT_TOUCH_REGION +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.FULLSCREEN_TASKBAR_WINDOW +import com.android.launcher3.taskbar.TaskbarInsetsController.DebugTouchableRegion.Companion.ICONS_INVISIBLE +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarInsetsControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var taskbarInsetsController: TaskbarInsetsController + @InjectController lateinit var taskbarStashController: TaskbarStashController + + private val taskbarContext by taskbarUnitTestRule::activityContext + + @Test + fun imeShowing_taskbarWindowUntouchable() { + runOnMainSync { taskbarContext.updateSysuiStateFlags(SYSUI_STATE_IME_VISIBLE, false) } + runOnMainSync { + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableReason) + .isEqualTo(ICONS_INVISIBLE) + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableInsets) + .isEqualTo(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION) + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableBounds.isEmpty) + .isTrue() + } + } + + @Test + @TaskbarMode(TRANSIENT) + fun imeShowing_transientTaskbarUnstashed_taskbarWindowTouchable() { + runOnMainSync { + taskbarContext.updateSysuiStateFlags(SYSUI_STATE_IME_VISIBLE, false) + taskbarStashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(taskbarStashController.stashDuration) + } + runOnMainSync { + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableReason) + .isEqualTo(DEFAULT_TOUCH_REGION) + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableInsets) + .isEqualTo(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION) + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableBounds.isEmpty) + .isFalse() + } + } + + @Test + fun windowFullscreen_entireTaskbarWindowTouchable() { + runOnMainSync { taskbarContext.setTaskbarWindowFullscreen(true) } + runOnMainSync { + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableReason) + .isEqualTo(FULLSCREEN_TASKBAR_WINDOW) + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableInsets) + .isEqualTo(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME) + } + } + + @Test + fun windowFullscreen_imeShowing_entireTaskbarWindowTouchable() { + runOnMainSync { + taskbarContext.setTaskbarWindowFullscreen(true) + taskbarContext.updateSysuiStateFlags(SYSUI_STATE_IME_VISIBLE, false) + } + runOnMainSync { + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableReason) + .isEqualTo(FULLSCREEN_TASKBAR_WINDOW) + assertThat(taskbarInsetsController.debugTouchableRegion.lastSetTouchableInsets) + .isEqualTo(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME) + } + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt similarity index 97% rename from quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt index e619e7cd38..24310202ca 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarKeyguardControllerTest.kt @@ -16,6 +16,7 @@ package com.android.launcher3.taskbar import android.app.KeyguardManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING @@ -23,6 +24,7 @@ import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_B import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -30,6 +32,7 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +@RunWith(AndroidJUnit4::class) class TaskbarKeyguardControllerTest : TaskbarBaseTestCase() { private val baseDragLayer: TaskbarDragLayer = mock() diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java index 0f06d98740..a1b341d763 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java @@ -4,6 +4,8 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y; @@ -13,19 +15,28 @@ import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IM import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS; import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; +import static com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.graphics.Rect; import android.os.Handler; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.Flags; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -34,11 +45,14 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TouchInteractionService; -import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.systemui.contextualeducation.GestureType; 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.MockitoAnnotations; @@ -49,12 +63,13 @@ public class TaskbarNavButtonControllerTest { @Mock SystemUiProxy mockSystemUiProxy; + @Mock TouchInteractionService mockService; @Mock Handler mockHandler; @Mock - AssistUtils mockAssistUtils; + ContextualSearchInvoker mockContextualSearchInvoker; @Mock StatsLogManager mockStatsLogManager; @Mock @@ -66,16 +81,19 @@ public class TaskbarNavButtonControllerTest { @Mock View mockView; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private int mHomePressCount; private int mOverviewToggleCount; private final TaskbarNavButtonCallbacks mCallbacks = new TaskbarNavButtonCallbacks() { @Override - public void onNavigateHome() { + public void onNavigateHome(int displayId) { mHomePressCount++; } @Override - public void onToggleOverview() { + public void onToggleOverview(int displayId) { mOverviewToggleCount++; } }; @@ -93,24 +111,51 @@ public class TaskbarNavButtonControllerTest { when(mockTaskbarControllers.getTaskbarActivityContext()) .thenReturn(mockTaskbarActivityContext); doReturn(mockStatsLogManager).when(mockTaskbarActivityContext).getStatsLogManager(); + when(mockTaskbarActivityContext.getDisplayId()).thenReturn(DISPLAY_ID); mNavButtonController = new TaskbarNavButtonController( mockService, mCallbacks, mockSystemUiProxy, mockHandler, - mockAssistUtils); + mockContextualSearchInvoker); } @Test public void testPressBack() { mNavButtonController.onButtonClick(BUTTON_BACK, mockView); - verify(mockSystemUiProxy, times(1)).onBackPressed(); + verify(mockSystemUiProxy, times(1)).onBackEvent(null); + } + + @Test + public void testPressBack_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_BACK, mockView); + verify(mockSystemUiProxy, times(1)) + .updateContextualEduStats(/* isTrackpad= */ eq(false), eq(GestureType.BACK)); } @Test public void testPressImeSwitcher() { + mNavButtonController.init(mockTaskbarControllers); mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView); + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); + verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed(); + verify(mockSystemUiProxy, never()).onImeSwitcherLongPress(); + } + + @Test + public void testLongPressImeSwitcher() { + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.onButtonLongClick(BUTTON_IME_SWITCH, mockView); + verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); + verify(mockSystemUiProxy, never()).onImeSwitcherPressed(); + if (Flags.imeSwitcherRevamp()) { + verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); + verify(mockSystemUiProxy, times(1)).onImeSwitcherLongPress(); + } else { + verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); + verify(mockSystemUiProxy, never()).onImeSwitcherLongPress(); + } } @Test @@ -129,40 +174,40 @@ public class TaskbarNavButtonControllerTest { @Test public void testLongPressHome_enabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, times(1)).startAssistant(any()); } @Test public void testLongPressHome_enabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withoutOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @Test public void testLongPressHome_disabled_withOverride() { mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/); - when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true); + when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true); mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView); - verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt()); + verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt()); verify(mockSystemUiProxy, never()).startAssistant(any()); } @@ -172,12 +217,26 @@ public class TaskbarNavButtonControllerTest { assertThat(mHomePressCount).isEqualTo(1); } + @Test + public void testPressHome_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + verify(mockSystemUiProxy, times(1)) + .updateContextualEduStats(/* isTrackpad= */ eq(false), eq(GestureType.HOME)); + } + @Test public void testPressRecents() { mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); assertThat(mOverviewToggleCount).isEqualTo(1); } + @Test + public void testPressRecents_updateContextualEduData() { + mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); + verify(mockSystemUiProxy, times(1)) + .updateContextualEduStats(/* isTrackpad= */ eq(false), eq(GestureType.OVERVIEW)); + } + @Test public void testPressRecentsWithScreenPinned_noNavigationToOverview() { mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING); @@ -282,4 +341,57 @@ public class TaskbarNavButtonControllerTest { verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS); verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); } + + @Test + @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV) + public void testPredictiveBackInvoked() { + mNavButtonController.init(mockTaskbarControllers); + ArgumentCaptor keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false); + verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture()); + verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false); + verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, false); + } + + @Test + @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV) + public void testPredictiveBackCancelled() { + mNavButtonController.init(mockTaskbarControllers); + ArgumentCaptor keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, true); + verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture()); + verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false); + verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, true); + } + + @Test + @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV) + public void testButtonsDisabledWhileBackPressed() { + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false); + mNavButtonController.onButtonClick(BUTTON_HOME, mockView); + mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView); + mNavButtonController.onButtonLongClick(BUTTON_A11Y, mockView); + mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView); + mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false); + assertThat(mHomePressCount).isEqualTo(0); + verify(mockSystemUiProxy, never()).notifyAccessibilityButtonLongClicked(); + assertThat(mOverviewToggleCount).isEqualTo(0); + verify(mockSystemUiProxy, never()).onImeSwitcherPressed(); + } + + @Test + public void testOnRecentsButtonLayoutChanged() { + Rect rect = new Rect(10, 20, 30, 40); + mNavButtonController.init(mockTaskbarControllers); + mNavButtonController.onRecentsButtonLayoutChanged(rect); + verify(mockSystemUiProxy).notifyRecentsButtonPositionChanged(eq(rect)); + } + + private void verifyKeyEvent(KeyEvent keyEvent, int action, boolean isCancelled) { + assertEquals(isCancelled, keyEvent.isCanceled()); + assertEquals(action, KeyEvent.ACTION_DOWN, keyEvent.getAction()); + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt new file mode 100644 index 0000000000..f4b1521649 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt @@ -0,0 +1,810 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import android.app.WindowConfiguration +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.BubbleTextView +import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR +import com.android.launcher3.R +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.dagger.LauncherComponentProvider.appComponent +import com.android.launcher3.model.BgDataModel +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.TaskItemInfo +import com.android.launcher3.model.data.WorkspaceData +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.popup.SystemShortcut +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.rules.AllTaskbarSandboxModules +import com.android.launcher3.taskbar.rules.MockedRecentsModelHelper +import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule +import com.android.launcher3.taskbar.rules.SandboxParams +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarSandboxComponent +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.Preconditions.assertNotNull +import com.android.launcher3.util.TestUtil.getOnUiThread +import com.android.quickstep.RecentsModel +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SingleTask.Companion.createTaskItemInfo +import com.android.systemui.shared.recents.model.Task +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS +import com.android.window.flags.Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU +import com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_OVERFLOW +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR +import com.android.wm.shell.desktopmode.IDesktopTaskListener +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import java.util.function.Predicate +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +@EnableFlags( + FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_BUBBLE_BAR, + FLAG_ENABLE_TASKBAR_OVERFLOW, +) +@DisableFlags(FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR) +class TaskbarOverflowTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + val mockRecentsModelHelper: MockedRecentsModelHelper = MockedRecentsModelHelper() + + @get:Rule(order = 1) + val context = + TaskbarWindowSandboxContext.create( + SandboxParams( + { + spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { proxy -> + doAnswer { desktopTaskListener = it.getArgument(0) } + .whenever(proxy) + .setDesktopTaskListener(anyOrNull()) + } + }, + DaggerTaskbarOverflowComponent.builder() + .bindRecentsModel(mockRecentsModelHelper.mockRecentsModel), + ) + ) + + @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(mockRecentsModelHelper) + + @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context) + + @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this) + + @get:Rule(order = 5) + val taskbarUnitTestRule = TaskbarUnitTestRule(this, context, this::onControllersInitialized) + + @InjectController lateinit var taskbarViewController: TaskbarViewController + @InjectController lateinit var recentAppsController: TaskbarRecentAppsController + @InjectController lateinit var bubbleBarViewController: BubbleBarViewController + @InjectController lateinit var bubbleStashController: BubbleStashController + @InjectController lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController + + private val desktopVisibilityController: DesktopVisibilityController + get() = DesktopVisibilityController.INSTANCE[context] + + private var desktopTaskListener: IDesktopTaskListener? = null + private val modelCallback = ModelCallbacks() + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private var currentControllerInitCallback: () -> Unit = {} + set(value) { + runOnMainSync { value.invoke() } + field = value + } + + private fun onControllersInitialized() { + runOnMainSync { + if (!recentAppsController.canShowRunningApps) { + recentAppsController.onDestroy() + recentAppsController.canShowRunningApps = true + recentAppsController.init( + taskbarUnitTestRule.activityContext.controllers, + emptyList(), + ) + } + + currentControllerInitCallback.invoke() + } + } + + @Before + fun ensureRunningAppsShowing() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + } + + @Test + @TaskbarMode(PINNED) + fun testTaskbarWithMaxNumIcons_pinned() { + addRunningAppsAndVerifyOverflowState(0) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testTaskbarWithMaxNumIcons_transient() { + addRunningAppsAndVerifyOverflowState(0) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(PINNED) + fun testOverflownTaskbar_pinned() { + addRunningAppsAndVerifyOverflowState(5) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testOverflownTaskbar_transient() { + addRunningAppsAndVerifyOverflowState(5) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing) + } + + @Test + @TaskbarMode(PINNED) + fun testOverflownTaskbarWithNoSpaceForRecentApps_pinned() { + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + + // Create two "recent" desktop tasks, and then add enough hotseat items so the taskbar + // reaches max number of items with hotseat item icons, all apps and divider icons only. + // I.e. so all desktop tasks are in taskbar overflow. + createDesktopTask(2) + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + taskbarView.updateItems( + createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount), + recentAppsController.shownTasks, + ) + } + + // Verify that taskbar overflow view is shown (eventhough it exceeds max taskbar icons). + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumberOfTaskbarIcons + 1) + assertThat(taskbarOverflowIconIndex).isEqualTo(maxNumberOfTaskbarIcons) + assertThat(overflowItems).containsExactlyElementsIn(0..1) + } + + @Test + @TaskbarMode(PINNED) + fun testOverflownTaskbarWithNoSpaceForRecentApps_singleRecent_pinned() { + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + + // Create a "recent" desktop task, and then add enough hotseat items so the taskbar + // reaches max number of items with hotseat item icons, all apps and divider icons only. + // I.e. so the single desktop tasks is in taskbar overflow. + createDesktopTask(1) + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + val hotseatItems = createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount) + + taskbarView.updateItems( + recentAppsController.updateHotseatItemInfos(hotseatItems as Array), + recentAppsController.shownTasks, + ) + } + + // Verify that recent task is shown (eventhough it exceeds max taskbar icons), and that + // the taskbar overflow view is not added for the single recent app. + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumberOfTaskbarIcons + 1) + assertThat(taskbarOverflowIconIndex).isEqualTo(-1) + } + + @Test + @TaskbarMode(PINNED) + fun testBubbleBarReducesTaskbarMaxNumIcons_pinned() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testBubbleBarReducesTaskbarMaxNumIcons_transient() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin) + .isAtLeast( + navButtonEndSpacing + + bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt() + ) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testBubbleBarReducesTaskbarMaxNumIcons_transientBubbleInitiallyStashed() { + var initialMaxNumIconViews = maxNumberOfTaskbarIcons + assertThat(initialMaxNumIconViews).isGreaterThan(0) + currentControllerInitCallback = { + bubbleStashController.stashBubbleBarImmediate() + bubbleBarViewController.setHiddenForBubbles(false) + } + + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews) + + assertThat(taskbarIconsCentered).isTrue() + assertThat(taskbarEndMargin) + .isAtLeast( + navButtonEndSpacing + + bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt() + ) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testStashingBubbleBarMaintainsMaxNumIcons_transient() { + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2) + + runOnMainSync { bubbleStashController.stashBubbleBarImmediate() } + assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + } + + @Test + @TaskbarMode(PINNED) + fun testHidingBubbleBarIncreasesMaxNumIcons_pinned() { + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) } + runOnMainSync { animatorTestRule.advanceTimeBy(150) } + + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testHidingBubbleBarIncreasesMaxNumIcons_transient() { + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(false) } + + val initialNumIcons = currentNumberOfTaskbarIcons + val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5) + + currentControllerInitCallback = { bubbleBarViewController.setHiddenForBubbles(true) } + runOnMainSync { animatorTestRule.advanceTimeBy(150) } + + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2)) + + assertThat(taskbarIconsCentered).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testPressingOverflowButtonOpensKeyboardQuickSwitch() { + val maxNumIconViews = maxNumberOfTaskbarIcons + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + + val targetOverflowSize = 5 + val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize + createDesktopTask(createdTasks) + + assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount) + assertThat(getOverflowIconTooltipText()).isEqualTo("Other recent apps") + + tapOverflowIcon() + // Keyboard quick switch view is shown only after list of recent task is asynchronously + // retrieved from the recents model. + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + + assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue() + assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }) + .containsExactlyElementsIn(0..targetOverflowSize) + assertThat(getOverflowIconTooltipText()).isNull() + + tapOverflowIcon() + assertThat(keyboardQuickSwitchController.isShown).isFalse() + assertThat(getOverflowIconTooltipText()).isEqualTo("Other recent apps") + } + + @Test + @TaskbarMode(PINNED) + fun testHotseatItemTasksNotShownInRecents() { + val maxNumIconViews = maxNumberOfTaskbarIcons + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + val hotseatItems = createHotseatItems(1) + + val targetOverflowSize = 5 + val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize + createDesktopTaskWithTasksFromPackages( + listOf("fake") + + listOf(hotseatItems[0]?.targetPackage ?: "") + + List(createdTasks - 2) { "fake" } + ) + + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + taskbarView.updateItems( + recentAppsController.updateHotseatItemInfos(hotseatItems as Array), + recentAppsController.shownTasks, + ) + } + + assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount + hotseatItems.size) + assertThat(overflowItems) + .containsExactlyElementsIn(listOf(0) + (2..targetOverflowSize + 1).toList()) + } + + @Test + @TaskbarMode(PINNED) + fun testHotseatItemTasksNotShownInKQS() { + val maxNumIconViews = maxNumberOfTaskbarIcons + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + val hotseatItems = createHotseatItems(1) + + val targetOverflowSize = 5 + val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize + createDesktopTaskWithTasksFromPackages( + listOf("fake") + + listOf(hotseatItems[0]?.targetPackage ?: "") + + List(createdTasks - 2) { "fake" } + ) + + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + taskbarView.updateItems( + recentAppsController.updateHotseatItemInfos(hotseatItems as Array), + recentAppsController.shownTasks, + ) + } + + tapOverflowIcon() + // Keyboard quick switch view is shown only after list of recent task is asynchronously + // retrieved from the recents model. + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + + assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue() + assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }) + .containsExactlyElementsIn(listOf(0) + (2..targetOverflowSize + 1).toList()) + } + + @Test + @TaskbarMode(PINNED) + fun testFullscreenTasksNotShownInKQS() { + val maxNumIconViews = maxNumberOfTaskbarIcons + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + val hotseatItems = createHotseatItems(1) + + val targetOverflowSize = 5 + val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize + createFullscreenAndDesktopTasksFromPackages( + listOf("fakeFullscreen"), + listOf("fake") + + listOf(hotseatItems[0]?.targetPackage ?: "") + + List(createdTasks - 2) { "fake" }, + ) + + runOnMainSync { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + taskbarView.updateItems( + recentAppsController.updateHotseatItemInfos(hotseatItems as Array), + recentAppsController.shownTasks, + ) + } + + tapOverflowIcon() + // Keyboard quick switch view is shown only after list of recent task is asynchronously + // retrieved from the recents model. + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + + assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue() + // Taskbar is in overflow by `targetOverflowSize`, so overflow UI should have + // `targetOverflowSize + 1` items, to account for a spot in taskbar taken by the overflow + // icon. Task IDs for running desktop apps start at 1 - 0 is used for fullscreen task. + assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() }) + .containsExactlyElementsIn(listOf(1) + (3..targetOverflowSize + 2).toList()) + } + + @Test + @TaskbarMode(PINNED) + @EnableFlags(FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU) + fun pinToTaskbarShortcut_unpinPinnedItem() { + // Create two tasks and two pinned items. + createDesktopTask(2) + val hotseatItems = createHotseatItems(2) + var shortcut: SystemShortcut<*>? = null + var hotseatIcon: BubbleTextView? = null + runOnMainSync { + val taskbarView = setUpTaskbarAndModelCallback(hotseatItems) + hotseatIcon = + taskbarView.iconViews.filterIsInstance().first { + it.tag is WorkspaceItemInfo + } + shortcut = + taskbarContext.controllers.taskbarPopupController.createPinShortcut( + taskbarContext, + hotseatIcon!!.tag as ItemInfo, + hotseatIcon, + ) as SystemShortcut<*> + } + assertNotNull(shortcut) + runOnMainSync { shortcut?.onClick(hotseatIcon) } + // After unpinning the first item, only the second app is left. + assertThat(modelCallback.hotseatItems.map { info -> info.title }) + .isEqualTo(listOf("Test App 1")) + // The unpinned app doesn't have a task so the shown tasks won't change. + assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }) + .isEqualTo(listOf(0, 1)) + } + + @Test + @TaskbarMode(PINNED) + @EnableFlags(FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU) + fun pinToTaskbarShortcut_unpinPinnedItemWithTask() { + // Create two hotseat items with a task for both of them respectively. + var hotseatItems = + createHotseatItems(2).mapIndexed { idx, item -> TaskItemInfo(idx, item) }.toTypedArray() + createDesktopTaskWithTasksFromPackages(hotseatItems.mapNotNull { it.targetPackage }) + var shortcut: SystemShortcut<*>? = null + var hotseatIcon: BubbleTextView? = null + runOnMainSync { + val taskbarView = setUpTaskbarAndModelCallback(hotseatItems.map { it }.toTypedArray()) + hotseatIcon = + taskbarView.iconViews.filterIsInstance().first { + it.tag is WorkspaceItemInfo + } + shortcut = + taskbarContext.controllers.taskbarPopupController.createPinShortcut( + taskbarContext, + hotseatIcon!!.tag as ItemInfo, + hotseatIcon, + ) as SystemShortcut<*> + } + // Before unpinning the app, both of the apps should be pinned and no shown task available. + assertThat(modelCallback.hotseatItems.map { info -> info.title }) + .isEqualTo(listOf("Test App 0", "Test App 1")) + assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }) + .isEqualTo(emptyList()) + assertNotNull(shortcut) + runOnMainSync { shortcut?.onClick(hotseatIcon) } + // After unpinning the app, app 0 is removed and its task is shown as a recent task. + assertThat(modelCallback.hotseatItems.map { info -> info.title }) + .isEqualTo(listOf("Test App 1")) + assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }).isEqualTo(listOf(0)) + } + + @Test + @TaskbarMode(PINNED) + @EnableFlags(FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU) + fun pinToTaskbarShortcut_pinRecentTask() { + // Create two tasks and two pinned items. + createDesktopTask(2) + val hotseatItems = createHotseatItems(2) + + var shortcut: SystemShortcut<*>? = null + var recentTaskIcon: BubbleTextView? = null + runOnMainSync { + val taskbarView = setUpTaskbarAndModelCallback(hotseatItems) + // Get the first recent task icon + recentTaskIcon = + taskbarView.iconViews.filterIsInstance().first { + it.tag is GroupTask + } + val recentTaskInfo = + createTaskItemInfo( + recentTaskIcon!!.tag as SingleTask, + WorkspaceItemInfo().apply { + title = "Test App 2" + intent = Intent().apply { `package` = "fake" } + }, + ) + shortcut = + taskbarContext.controllers.taskbarPopupController.createPinShortcut( + taskbarContext, + recentTaskInfo, + recentTaskIcon, + ) as SystemShortcut<*> + } + assertNotNull(shortcut) + runOnMainSync { shortcut?.onClick(recentTaskIcon) } + + // After pinning the recent task, it should be included in the hotseat items. + assertThat(modelCallback.hotseatItems.map { info -> info.title }) + .isEqualTo(listOf("Test App 0", "Test App 1", "Test App 2")) + // As the task is pinned, the shown tasks should remove it from the list + assertThat(recentAppsController.shownTasks.map { it.tasks[0].key.id }).isEqualTo(listOf(1)) + } + + private fun setUpTaskbarAndModelCallback(hotseatItems: Array): TaskbarView { + val taskbarView: TaskbarView = + taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + taskbarView.updateItems(hotseatItems, recentAppsController.shownTasks) + modelCallback.recentAppsController = recentAppsController + context.baseContext.appComponent.launcherAppState.model.addCallbacks(modelCallback) + modelCallback.bindItemsAdded(hotseatItems.toList()) + return taskbarView + } + + private fun createDesktopTask(tasksToAdd: Int) { + createDesktopTaskWithTasksFromPackages((0..) { + createFullscreenAndDesktopTasksFromPackages(emptyList(), packages) + } + + private fun createFullscreenAndDesktopTasksFromPackages( + fullscreenPackages: List, + desktopPackages: List, + ) { + val defaultDisplayId = context.displayId + val tasks: List = + fullscreenPackages.mapIndexed({ index, p -> + SingleTask( + Task( + Task.TaskKey( + index, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN, + Intent().apply { `package` = p }, + ComponentName(p, ""), + Process.myUserHandle().identifier, + 2000, + ) + ) + ) + }) + + val desktopTasks = + desktopPackages.mapIndexed({ index, p -> + Task( + Task.TaskKey( + index + fullscreenPackages.size, + WindowConfiguration.WINDOWING_MODE_FREEFORM, + Intent().apply { `package` = p }, + ComponentName(p, ""), + Process.myUserHandle().identifier, + 2000, + ) + ) + }) + + recentsModel.updateRecentTasks( + tasks + listOf(DesktopTask(deskId = 0, defaultDisplayId, desktopTasks)) + ) + for (task in 1..desktopTasks.size) { + desktopTaskListener?.onTasksVisibilityChanged(defaultDisplayId, task) + } + runOnMainSync { recentsModel.resolvePendingTaskRequests() } + } + + private val navButtonEndSpacing: Int + get() { + return taskbarUnitTestRule.activityContext.resources.getDimensionPixelSize( + taskbarUnitTestRule.activityContext.deviceProfile.inv.inlineNavButtonsEndSpacing + ) + } + + private val taskbarOverflowIconIndex: Int + get() { + return getOnUiThread { + taskbarViewController.iconViews.indexOfFirst { it is TaskbarOverflowView } + } + } + + private val maxNumberOfTaskbarIcons: Int + get() = getOnUiThread { taskbarViewController.maxNumIconViews } + + private val currentNumberOfTaskbarIcons: Int + get() = getOnUiThread { taskbarViewController.iconViews.size } + + private val taskbarIconsCentered: Boolean + get() { + return getOnUiThread { + val iconLayoutBounds = + taskbarViewController.transientTaskbarIconLayoutBoundsInParent + val availableWidth = + taskbarUnitTestRule.activityContext.deviceProfile.deviceProperties.widthPx + iconLayoutBounds.left - (availableWidth - iconLayoutBounds.right) < 2 + } + } + + private val taskbarEndMargin: Int + get() { + return getOnUiThread { + taskbarUnitTestRule.activityContext.deviceProfile.deviceProperties.widthPx - + taskbarViewController.transientTaskbarIconLayoutBoundsInParent.right + } + } + + private val overflowItems: List + get() { + return getOnUiThread { + val overflowIcon = + taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView } + + if (overflowIcon is TaskbarOverflowView) { + overflowIcon.itemIds + } else { + emptyList() + } + } + } + + private fun tapOverflowIcon() { + runOnMainSync { + val overflowIcon = + taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView } + assertThat(overflowIcon?.callOnClick()).isTrue() + } + } + + private fun getOverflowIconTooltipText(): String? { + return getOnUiThread { + val overflowIcon = + taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView } + (overflowIcon as? TaskbarOverflowView)?.getTextForTooltipPopup() + } + } + + /** + * Adds enough running apps for taskbar to enter overflow of `targetOverflowSize`, and verifies + * * max number of icons in the taskbar remains unchanged + * * number of icons in the taskbar is at most max number of icons + * * whether the taskbar overflow icon is shown, and its position in taskbar. + * + * Returns max number of icons. + */ + private fun addRunningAppsAndVerifyOverflowState(targetOverflowSize: Int): Int { + val maxNumIconViews = maxNumberOfTaskbarIcons + assertThat(maxNumIconViews).isGreaterThan(0) + // Assume there are at least all apps and divider icon, as they would appear once running + // apps are added, even if not present initially. + val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2) + assertThat(initialIconCount).isLessThan(maxNumIconViews) + + createDesktopTask(maxNumIconViews - initialIconCount + targetOverflowSize) + + assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews) + assertThat(taskbarOverflowIconIndex) + .isEqualTo(if (targetOverflowSize > 0) initialIconCount else -1) + if (targetOverflowSize > 0) { + assertThat(overflowItems).containsExactlyElementsIn(0..targetOverflowSize) + } + return maxNumIconViews + } + + private class ModelCallbacks : BgDataModel.Callbacks { + var hotseatItems = mutableListOf() + var recentAppsController: TaskbarRecentAppsController? = null + + override fun bindCompleteModel(itemIdMap: WorkspaceData, isBindingSync: Boolean) = + bindItemsAdded(itemIdMap.toList()) + + override fun bindItemsAdded(items: List) { + runOnMainSync { + items + .filter { item -> + item is WorkspaceItemInfo && + !hotseatItems.any { it.targetPackage == item.targetPackage } + } + .forEach { item -> hotseatItems.add(item as WorkspaceItemInfo) } + recentAppsController?.updateHotseatItemInfos(hotseatItems.toTypedArray()) + } + } + + override fun bindWorkspaceComponentsRemoved(matcher: Predicate) { + runOnMainSync { + for (i in hotseatItems.size - 1 downTo 0) { + if (matcher.test(hotseatItems[i])) { + hotseatItems.removeAt(i) + } + } + recentAppsController?.updateHotseatItemInfos(hotseatItems.toTypedArray()) + } + } + } +} + +/** TaskbarOverflowComponent used to bind the RecentsModel. */ +@LauncherAppSingleton +@Component(modules = [AllTaskbarSandboxModules::class]) +interface TaskbarOverflowComponent : TaskbarSandboxComponent { + + @Component.Builder + interface Builder : TaskbarSandboxComponent.Builder { + @BindsInstance fun bindRecentsModel(model: RecentsModel): Builder + + override fun build(): TaskbarOverflowComponent + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPinningControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPinningControllerTest.kt new file mode 100644 index 0000000000..e1cad71592 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPinningControllerTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.animation.AnimatorTestRule +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_PINNING_POPUP +import com.android.launcher3.R +import com.android.launcher3.popup.ArrowPopup.OPEN_DURATION_U +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.customization.TaskbarDividerContainer +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarPinningControllerTest { + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + @get:Rule(order = 2) val animatorTestRule = AnimatorTestRule(this) + + @InjectController lateinit var pinningController: TaskbarPinningController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private lateinit var taskbarView: TaskbarView + private lateinit var dividerIcon: TaskbarDividerContainer + + @Before + fun setup() { + taskbarContext.controllers.uiController.init(taskbarContext.controllers) + runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) } + + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), emptyList()) + dividerIcon = requireNotNull(taskbarView.taskbarDividerViewContainer) + } + } + + @Test + fun showPinningView() { + assertThat(hasPinningPopUp).isFalse() + runOnMainSync { pinningController.showPinningView(dividerIcon) } + runOnMainSync { + // Animation has started. Advance to end of animation. + animatorTestRule.advanceTimeBy(OPEN_DURATION_U.toLong()) + } + assertThat(hasPinningPopUp).isTrue() + } + + private val hasPinningPopUp: Boolean + get() { + return AbstractFloatingView.hasOpenView(taskbarContext, TYPE_TASKBAR_PINNING_POPUP) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt new file mode 100644 index 0000000000..1cdc4fcb88 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarPopupControllerTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.content.ComponentName +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.util.SparseArray +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.BubbleTextView +import com.android.launcher3.Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR +import com.android.launcher3.LauncherSettings +import com.android.launcher3.R +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatWorkspaceItem +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createTestWorkspaceItem +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.quickstep.util.GroupTask +import com.android.window.flags.Flags.FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU +import com.google.common.truth.Truth.assertThat +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +@DisableFlags(FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR) +class TaskbarPopupControllerTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var popupController: TaskbarPopupController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private val desktopVisibilityController: DesktopVisibilityController + get() = DesktopVisibilityController.INSTANCE[context] + + private lateinit var taskbarView: TaskbarView + private lateinit var hotseatIcon: BubbleTextView + private lateinit var recentTaskIcon: BubbleTextView + + @Before + fun setup() { + taskbarContext.controllers.uiController.init(taskbarContext.controllers) + runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) } + + val hotseatItems = arrayOf(createHotseatWorkspaceItem()) + popupController.setApps( + hotseatItems + .map { item -> AppInfo(item.targetComponent, item.title, item.user, item.intent) } + .toTypedArray() + ) + popupController.setHotseatInfosList(SparseArray()) + val recentItems = createRecents(2) + runOnMainSync { + taskbarView.updateItems(hotseatItems, recentItems) + hotseatIcon = + taskbarView.iconViews.filterIsInstance().first { + it.tag is WorkspaceItemInfo + } + recentTaskIcon = + taskbarView.iconViews.filterIsInstance().first { + it.tag is GroupTask + } + } + } + + @Test + fun showForIcon_hotseatItem() { + assertThat(hasPopupMenu()).isFalse() + runOnMainSync { popupController.showForIcon(hotseatIcon) } + assertThat(hasPopupMenu()).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_PINNING_APP_WITH_CONTEXT_MENU) + fun showForIcon_recentTask() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + assertThat(hasPopupMenu()).isFalse() + runOnMainSync { popupController.showForIcon(recentTaskIcon) } + assertThat(hasPopupMenu()).isTrue() + } + + @Test + fun createPinShortcut_itemAlreadyPinned_returnsUnpinShortcut() { + val hotseatItems = SparseArray() + val appUser = android.os.Process.myUserHandle() + val appAIntent = Intent().setComponent(ComponentName("com.example.app", "AppAActivity")) + + val itemFromAllApps = + createTestWorkspaceItem( + 0, + "AppA", + appAIntent, + appUser, + LauncherSettings.Favorites.CONTAINER_ALL_APPS, + ) + + val pinnedItemInHotseat = + createTestWorkspaceItem( + 1, + "AppA", + appAIntent, + appUser, + LauncherSettings.Favorites.CONTAINER_HOTSEAT, + ) + + hotseatItems.put(0, pinnedItemInHotseat) + popupController.setHotseatInfosList(hotseatItems) + val allAppsAppIcon = Mockito.mock(BubbleTextView::class.java) + + val shortcut = + popupController.createPinShortcut(taskbarContext, itemFromAllApps, allAppsAppIcon) + Assert.assertNotNull("Shortcut should not be null", shortcut) + Assert.assertTrue( + "Shortcut should be PinToTaskbarShortcut", + shortcut is PinToTaskbarShortcut<*>, + ) + Assert.assertFalse((shortcut as PinToTaskbarShortcut<*>).mIsPin) + } + + private fun hasPopupMenu(): Boolean { + return AbstractFloatingView.hasOpenView( + taskbarContext, + AbstractFloatingView.TYPE_ACTION_POPUP, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt new file mode 100644 index 0000000000..f3d828a406 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt @@ -0,0 +1,1323 @@ +/* + * 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.launcher3.taskbar + +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.graphics.Rect +import android.os.Process +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.annotation.UiThreadTest +import com.android.internal.R +import com.android.launcher3.BubbleTextView.RunningAppState +import com.android.launcher3.Flags +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.TaskItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarRecentAppsController.TaskState +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.quickstep.RecentsModel +import com.android.quickstep.RecentsModel.RecentTasksChangedListener +import com.android.quickstep.TaskIconCache +import com.android.quickstep.util.DesktopTask +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SplitTask +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@UiThreadTest +@RunWith(LauncherMultivalentJUnit::class) +@EnableFlags(Flags.FLAG_ENABLE_MULTI_INSTANCE_MENU_TASKBAR) +class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { + + @get:Rule val mockitoRule = MockitoJUnit.rule() + @get:Rule val setFlagsRule = SetFlagsRule() + @get:Rule + val disableControllerForCertainTestsWatcher = + object : TestWatcher() { + override fun starting(description: Description) { + // Update canShowRunningAndRecentAppsAtInit before setUp() is called for each test. + canShowRunningAndRecentAppsAtInit = + description.methodName !in + listOf("canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled") + } + } + + @Mock private lateinit var mockIconCache: TaskIconCache + @Mock private lateinit var mockRecentsModel: RecentsModel + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources + + private var taskListChangeId: Int = 1 + + private lateinit var recentAppsController: TaskbarRecentAppsController + private lateinit var myUserHandle: UserHandle + private val USER_HANDLE_1 = UserHandle.of(1) + private val USER_HANDLE_2 = UserHandle.of(2) + + private var canShowRunningAndRecentAppsAtInit = true + private var recentTasksChangedListener: RecentTasksChangedListener? = null + + val recentShownTasks: List + get() = recentAppsController.shownTasks.flatMap { it.tasks } + + @Before + fun setUp() { + super.setup() + myUserHandle = Process.myUserHandle() + + // Set desktop mode supported + whenever(mockContext.getResources()).thenReturn(mockResources) + whenever(mockResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true) + + whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache) + whenever(mockRecentsModel.unregisterRecentTasksChangedListener(any())).then { + recentTasksChangedListener = null + it + } + whenever(taskbarDesktopModeController.isLauncherAnimationRunning).thenReturn(false) + recentAppsController = TaskbarRecentAppsController(mockContext, mockRecentsModel) + recentAppsController.canShowRunningApps = canShowRunningAndRecentAppsAtInit + recentAppsController.canShowRecentApps = canShowRunningAndRecentAppsAtInit + + // To ensure the initial getTasks() call is not seen as "loading" for the rest of the test, + // execute its callback. + doAnswer { + val callback: Consumer> = it.getArgument(1) + callback.accept(arrayListOf()) + taskListChangeId + } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentAppsController.init(taskbarControllers, emptyList()) + taskbarControllers.onPostInit() + + recentTasksChangedListener = + if (canShowRunningAndRecentAppsAtInit) { + val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java) + verify(mockRecentsModel) + .registerRecentTasksChangedListener(listenerCaptor.capture()) + listenerCaptor.value + } else { + verify(mockRecentsModel, never()).registerRecentTasksChangedListener(any()) + null + } + + // Make sure updateHotseatItemInfos() is called after commitRunningAppsToUI() + whenever(taskbarViewController.commitRunningAppsToUI()).then { + recentAppsController.updateHotseatItemInfos( + recentAppsController.shownHotseatItems.toTypedArray() + ) + } + } + + // See the TestWatcher rule at the top which sets canShowRunningAndRecentAppsAtInit = false. + @Test + fun canShowRunningAndRecentAppsAtInitIsFalse_getTasksNeverCalled() { + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + verify(mockRecentsModel, never()).getTasks(any(), any>>()) + } + + @Test + fun canShowRunningAndRecentAppsIsFalseAfterInit_getTasksOnlyCalledInInit() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + recentAppsController.canShowRunningApps = false + recentAppsController.canShowRecentApps = false + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = listOf(createTask(1, RUNNING_APP_PACKAGE_1)), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + // Verify that getTasks() was not called again after the init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + } + + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_dontCallGetTasks() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + + // getTasks() is only called two times overall (init + once more). + verify(mockRecentsModel, times(2)).getTasks(any(), any>>()) + } + + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_getTasksCalledWhenLoadingDone() { + val callbackCaptor = argumentCaptor>>() + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), callbackCaptor.capture()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + callbackCaptor.lastValue.accept(emptyList()) + + // getTasks() is called again now that the first getTasks() call finished. + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + + @Test + @DisableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_flagDisabled_callGetTasks() { + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), any>>()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + // By not invoking the callback passed to getTasks() we here emulate getTasks() loading. + + recentTasksChangedListener?.onRecentTasksChanged() + + // getTasks() is called once per onRecentTasksChanged() invocation (and once at init) + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + + @Test + @DisableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENT_TASKS_THROTTLE_BUGFIX) + fun recentTasksChanged_duringGetTasksLoading_flagDisabled_getTasksNotCalledWhenLoadingDone() { + val callbackCaptor = argumentCaptor>>() + // getTasks() should have been called once from init(). + verify(mockRecentsModel, times(1)).getTasks(any(), callbackCaptor.capture()) + // Override the mock answer for getTasks() so it doesn't call the callback immediately. + doAnswer { taskListChangeId } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + recentTasksChangedListener?.onRecentTasksChanged() + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + + callbackCaptor.lastValue.accept(emptyList()) + + // getTasks() is called once per onRecentTasksChanged() invocation (and once at init) + verify(mockRecentsModel, times(3)).getTasks(any(), any>>()) + } + + @Test + fun getDesktopItemState_nullItemInfo_returnsNotRunning() { + setInDesktopMode(true) + val taskState = recentAppsController.getDesktopItemState(/* itemInfo= */ null) + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getDesktopItemState_noItemPackage_returnsNotRunning() { + setInDesktopMode(true) + val taskState = recentAppsController.getDesktopItemState(ItemInfo()) + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getDesktopItemState_noMatchingTasks_returnsNotRunning() { + setInDesktopMode(true) + val taskState = recentAppsController.getDesktopItemState(createItemInfo("package")) + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getDesktopItemState_matchingVisibleTask_returnsVisible() { + setInDesktopMode(true) + val visibleTask = + PerDisplayRunningApps( + listOf(createTask(id = 1, "visiblePackage", isVisible = true)), + DEFAULT_DISPLAY, + ) + updateRecentTasks(runningTasks = listOf(visibleTask), recentTaskPackages = emptyList()) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("visiblePackage")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.RUNNING, taskId = 1)) + } + + @Test + fun getDesktopItemState_matchingVisibleTaskOnSecondaryDisplay_returnsVisible() { + setInDesktopMode(true) + val visibleTask1 = + PerDisplayRunningApps( + listOf(createTask(id = 1, "visiblePackage1", isVisible = false)), + DEFAULT_DISPLAY, + ) + val visibleTask2 = + PerDisplayRunningApps( + listOf(createTask(id = 2, "visiblePackage2", isVisible = true)), + DEFAULT_DISPLAY + 1, + ) + updateRecentTasks( + runningTasks = listOf(visibleTask1, visibleTask2), + recentTaskPackages = emptyList(), + ) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("visiblePackage2")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.RUNNING, taskId = 2)) + } + + @Test + fun getDesktopItemState_matchingMinimizedTask_returnsMinimized() { + setInDesktopMode(true) + val minimizedTask = + PerDisplayRunningApps( + listOf(createTask(id = 1, "minimizedPackage", isVisible = false)), + DEFAULT_DISPLAY, + ) + updateRecentTasks(runningTasks = listOf(minimizedTask), recentTaskPackages = emptyList()) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("minimizedPackage")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.MINIMIZED, taskId = 1)) + } + + @Test + fun getDesktopItemState_matchingMinimizedTaskOnSecondaryDisplay_returnsVisible() { + setInDesktopMode(true) + val visibleTask1 = + PerDisplayRunningApps( + listOf(createTask(id = 1, "visiblePackage1", isVisible = false)), + DEFAULT_DISPLAY, + ) + val visibleTask2 = + PerDisplayRunningApps( + listOf(createTask(id = 2, "visiblePackage2", isVisible = false)), + DEFAULT_DISPLAY + 1, + ) + updateRecentTasks( + runningTasks = listOf(visibleTask1, visibleTask2), + recentTaskPackages = emptyList(), + ) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("visiblePackage2")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.MINIMIZED, taskId = 2)) + } + + @Test + fun getDesktopItemState_matchingMinimizedAndRunningTask_returnsVisible() { + setInDesktopMode(true) + updateRecentTasks( + runningTasks = + listOf( + PerDisplayRunningApps( + listOf( + createTask(id = 1, "package", isVisible = false), + createTask(id = 2, "package", isVisible = true), + ), + DEFAULT_DISPLAY, + ) + ), + recentTaskPackages = emptyList(), + ) + + val taskState = recentAppsController.getDesktopItemState(createItemInfo("package")) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.RUNNING, taskId = 2)) + } + + @Test + fun getDesktopItemState_noMatchingUserId_returnsNotRunning() { + setInDesktopMode(true) + updateRecentTasks( + runningTasks = + listOf( + PerDisplayRunningApps( + listOf( + createTask(id = 1, "package", isVisible = false, USER_HANDLE_1), + createTask(id = 2, "package", isVisible = true, USER_HANDLE_1), + ), + DEFAULT_DISPLAY, + ) + ), + recentTaskPackages = emptyList(), + ) + + val taskState = + recentAppsController.getDesktopItemState(createItemInfo("package", USER_HANDLE_2)) + + assertThat(taskState).isEqualTo(TaskState(RunningAppState.NOT_RUNNING)) + } + + @Test + fun getRunningAppState_taskNotRunningOrMinimized_returnsNotRunning() { + setInDesktopMode(true) + updateRecentTasks(runningTasks = emptyList(), recentTaskPackages = emptyList()) + + assertThat(recentAppsController.getRunningAppState(taskId = 1)) + .isEqualTo(RunningAppState.NOT_RUNNING) + } + + @Test + fun getRunningAppState_taskNotVisible_returnsMinimized() { + setInDesktopMode(true) + val task1 = createTask(id = 1, packageName = RUNNING_APP_PACKAGE_1, isVisible = false) + val task2 = createTask(id = 2, packageName = RUNNING_APP_PACKAGE_1, isVisible = true) + updateRecentTasks( + runningTasks = listOf(PerDisplayRunningApps(listOf(task1, task2), DEFAULT_DISPLAY)), + recentTaskPackages = emptyList(), + ) + + assertThat(recentAppsController.getRunningAppState(taskId = 1)) + .isEqualTo(RunningAppState.MINIMIZED) + } + + @Test + fun getRunningAppState_taskNotVisible_returnsMinimizedForSecondaryDisplay() { + setInDesktopMode(true) + val task1 = createTask(id = 1, packageName = RUNNING_APP_PACKAGE_1, isVisible = false) + val task2 = createTask(id = 2, packageName = RUNNING_APP_PACKAGE_1, isVisible = true) + val task3 = createTask(id = 3, packageName = RUNNING_APP_PACKAGE_3, isVisible = false) + updateRecentTasks( + runningTasks = + listOf( + PerDisplayRunningApps(listOf(task1, task2), DEFAULT_DISPLAY), + PerDisplayRunningApps(listOf(task3), DEFAULT_DISPLAY + 1), + ), + recentTaskPackages = emptyList(), + ) + + assertThat(recentAppsController.getRunningAppState(taskId = 3)) + .isEqualTo(RunningAppState.MINIMIZED) + } + + @Test + fun getRunningAppState_taskVisible_returnsRunningForSecondaryDisplay() { + setInDesktopMode(true) + val task1 = createTask(id = 1, packageName = RUNNING_APP_PACKAGE_1, isVisible = false) + val task2 = createTask(id = 2, packageName = RUNNING_APP_PACKAGE_1, isVisible = true) + val task3 = createTask(id = 3, packageName = RUNNING_APP_PACKAGE_3, isVisible = true) + updateRecentTasks( + runningTasks = + listOf( + PerDisplayRunningApps(listOf(task1, task2), DEFAULT_DISPLAY), + PerDisplayRunningApps(listOf(task3), DEFAULT_DISPLAY + 1), + ), + recentTaskPackages = emptyList(), + ) + + assertThat(recentAppsController.getRunningAppState(taskId = 3)) + .isEqualTo(RunningAppState.RUNNING) + } + + @Test + fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() { + recentAppsController.canShowRunningApps = false + setInDesktopMode(true) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(hotseatPackages) + } + + @Test + fun updateHotseatItemInfos_cantShowRecent_notInDesktopMode_returnsAllHotseatItems() { + recentAppsController.canShowRecentApps = false + setInDesktopMode(false) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(hotseatPackages) + } + + @Test + fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() { + recentAppsController.canShowRunningApps = true + setInDesktopMode(true) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(expectedPackages) + } + + @Test + fun updateHotseatItemInfos_inDesktopMode_hotseatPackageHasRunningTask_hotseatItemLinksToTask() { + setInDesktopMode(true) + + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = listOf(createTask(id = 1, HOTSEAT_PACKAGE_1)), + recentTaskPackages = emptyList(), + ) + + assertThat(newHotseatItems).hasLength(2) + assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java) + assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java) + val hotseatItem1 = newHotseatItems[0] as TaskItemInfo + assertThat(hotseatItem1.taskId).isEqualTo(1) + } + + /** + * Tests that in desktop mode, when two tasks have the same package name and one is in the + * hotseat, only the hotseat item represents the app, and no duplicate is shown in recent apps. + */ + @Test + fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_onlyHotseatCoversTask() { + setInDesktopMode(true) + + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = + listOf( + createTask(id = 1, HOTSEAT_PACKAGE_1), + createTask(id = 2, HOTSEAT_PACKAGE_1), + ), + recentTaskPackages = emptyList(), + ) + + // The task is in Hotseat Items + assertThat(newHotseatItems).hasLength(2) + assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java) + assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java) + val hotseatItem1 = newHotseatItems[0] as TaskItemInfo + assertThat(hotseatItem1.targetPackage).isEqualTo(HOTSEAT_PACKAGE_1) + + // The other task of the same package is not in recentShownTasks + assertThat(recentShownTasks).isEmpty() + } + + @Test + fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() { + recentAppsController.canShowRecentApps = true + setInDesktopMode(false) + val newHotseatItems = + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) + assertThat(newHotseatItems.map { it?.targetPackage }) + .containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_cantShowRunning_inDesktopMode_shownTasks_returnsEmptyList() { + recentAppsController.canShowRunningApps = false + setInDesktopMode(true) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = + listOf( + createTask(id = 1, RUNNING_APP_PACKAGE_1), + createTask(id = 2, RUNNING_APP_PACKAGE_2), + ), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + } + + @Test + fun onRecentTasksChanged_cantShowRecent_notInDesktopMode_shownTasks_returnsEmptyList() { + recentAppsController.canShowRecentApps = false + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_noRecentTasks_shownTasks_returnsEmptyList() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = + listOf( + createTask(id = 1, RUNNING_APP_PACKAGE_1), + createTask(id = 2, RUNNING_APP_PACKAGE_2), + ), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_noRunningApps_shownTasks_returnsEmptyList() { + setInDesktopMode(true) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + assertThat(recentAppsController.shownTasks).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentShownTasks).containsExactlyElementsIn(listOf(task1, task2)) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() { + setInDesktopMode(false) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).isEmpty() + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2)) + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() { + setInDesktopMode(true) + val runningTasks = + listOf( + createTask(id = 1, HOTSEAT_PACKAGE_1), + createTask(id = 2, RUNNING_APP_PACKAGE_1), + createTask(id = 3, RUNNING_APP_PACKAGE_2), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2), + runningTasks = runningTasks, + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2, 3)) + assertThat(recentAppsController.minimizedTaskIds).isEmpty() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_allAppsRunningAndInvisibleAppsMinimized() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task3Minimized = createTask(id = 3, RUNNING_APP_PACKAGE_3, isVisible = false) + val runningTasks = listOf(task1, task2, task3Minimized) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = runningTasks, + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).containsExactly(1, 2, 3) + assertThat(recentAppsController.minimizedTaskIds).containsExactly(3) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_samePackage_differentTasks_severalRunningTasks() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task2, task1), + recentTaskPackages = emptyList(), + ) + + assertThat(recentShownTasks).isEqualTo(listOf(task1, task2)) + } + + /** + * Tests that when multiple instances of the same app are running in desktop mode and the app is + * not in the hotseat, only one instance is shown in the recent apps section. + */ + @Test + fun onRecentTasksChanged_inDesktopMode_multiInstance_noHotseat_shownTasksHasOneInstance() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + + // Assert that recentShownTasks contains only one instance of the app + assertThat(recentShownTasks).hasSize(1) + assertThat(recentShownTasks[0].key.packageName).isEqualTo(RUNNING_APP_PACKAGE_1) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_shownTasks_maintainsRecency() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Most recent packages, minus the currently running one (RECENT_PACKAGE_1). + assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2), + recentTaskPackages = emptyList(), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task2, task1, task3), + recentTaskPackages = emptyList(), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + val expectedOrder = + listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) + assertThat(shownPackages).isEqualTo(expectedOrder) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_addTask_shownTasks_maintainsRecency() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Most recent packages, minus the currently running one (RECENT_PACKAGE_1). + assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() { + setInDesktopMode(true) + val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1, task2, task3), + recentTaskPackages = emptyList(), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task2, task1), + recentTaskPackages = emptyList(), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + assertThat(shownPackages).isEqualTo(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_removeTask_shownTasks_maintainsRecency() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Most recent packages, minus the currently running one (RECENT_PACKAGE_3). + assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2)) + } + + @Test + fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() { + setInDesktopMode(false) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = recentTaskPackages, + ) + + setInDesktopMode(true) + recentTasksChangedListener!!.onRecentTasksChanged() + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + assertThat(shownPackages).containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + } + + @Test + fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() { + setInDesktopMode(true) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = recentTaskPackages, + ) + setInDesktopMode(false) + recentTasksChangedListener!!.onRecentTasksChanged() + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // Don't expect RECENT_PACKAGE_3 because it is currently running. + val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_hasRecentTasks_shownTasks_returnsRecentTasks() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3), + ) + val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames } + // RECENT_PACKAGE_3 is the top task (visible to user) so should be excluded. + val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() { + setInDesktopMode(false) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + val shownPackages = recentAppsController.shownTasks.map { it.packageNames } + // Only 2 recent tasks shown: Desktop Tile + 1 Recent Task + val desktopTilePackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) + val recentTaskPackages = listOf(RECENT_PACKAGE_1) + val expectedPackages = listOf(desktopTilePackages, recentTaskPackages) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_hasRecentAndSplitTasks_shownTasks_returnsRecentTaskAndPair() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + val shownPackages = recentAppsController.shownTasks.map { it.packageNames } + // Only 2 recent tasks shown: Pair + 1 Recent Task + val pairPackages = RECENT_SPLIT_PACKAGES_1.split("_") + val recentTaskPackages = listOf(RECENT_PACKAGE_1) + val expectedPackages = listOf(pairPackages, recentTaskPackages) + assertThat(shownPackages).containsExactlyElementsIn(expectedPackages) + } + + @Test + fun onRecentTasksChanged_notInDesktopMode_noActualChangeToRecents_commitRunningAppsToUI_notCalled() { + setInDesktopMode(false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_noActualChangeToRunning_commitRunningAppsToUI_notCalled() { + setInDesktopMode(true) + val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1) + val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(runningTask1, runningTask2), + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_onlyMinimizedChanges_commitRunningAppsToUI_isCalled() { + setInDesktopMode(true) + val task1Minimized = createTask(id = 1, RUNNING_APP_PACKAGE_1, isVisible = false) + val task2Visible = createTask(id = 2, RUNNING_APP_PACKAGE_2) + val task2Minimized = createTask(id = 2, RUNNING_APP_PACKAGE_2, isVisible = false) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1Minimized, task2Visible), + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + + // Call onRecentTasksChanged() again with a new minimized app, verify we update UI. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = listOf(task1Minimized, task2Minimized), + recentTaskPackages = emptyList(), + ) + + verify(taskbarViewController, times(2)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_hotseatAppStartsRunning_commitRunningAppsToUI_isCalled() { + setInDesktopMode(true) + val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) + val originalTasks = listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1)) + val newTasks = + listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1), createTask(id = 2, HOTSEAT_PACKAGE_1)) + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = originalTasks, + recentTaskPackages = emptyList(), + ) + verify(taskbarViewController, times(1)).commitRunningAppsToUI() + + // Call onRecentTasksChanged() again with a new running app, verify we update UI. + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = hotseatPackages, + runningTasks = newTasks, + recentTaskPackages = emptyList(), + ) + + verify(taskbarViewController, times(2)).commitRunningAppsToUI() + } + + @Test + fun onRecentTasksChanged_inDesktopMode_sameHotseatPackage_differentUser_isInShownTasks() { + setInDesktopMode(true) + val hotseatPackageUser = PackageUser(HOTSEAT_PACKAGE_1, USER_HANDLE_2) + val hotseatPackageUsers = listOf(hotseatPackageUser) + val runningTask = createTask(id = 1, HOTSEAT_PACKAGE_1, localUserHandle = USER_HANDLE_1) + val runningTasks = listOf(PerDisplayRunningApps(listOf(runningTask), DEFAULT_DISPLAY)) + prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers = hotseatPackageUsers, + runningTasks = runningTasks, + recentTaskPackages = emptyList(), + ) + assertThat(recentShownTasks).contains(runningTask) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_multipleDesktops() { + setInDesktopMode(true) + val hotseatPackageUsers = listOf(PackageUser(HOTSEAT_PACKAGE_1, USER_HANDLE_1)) + val defaultDisplayRunningTask = + createTask(id = 1, HOTSEAT_PACKAGE_1, localUserHandle = USER_HANDLE_1) + val secondaryDisplayRunningTask = + createTask(id = 2, RUNNING_APP_PACKAGE_1, localUserHandle = USER_HANDLE_1) + val runningTasks = + listOf( + PerDisplayRunningApps(listOf(defaultDisplayRunningTask), DEFAULT_DISPLAY), + PerDisplayRunningApps(listOf(secondaryDisplayRunningTask), DEFAULT_DISPLAY + 1), + ) + prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers = hotseatPackageUsers, + runningTasks = runningTasks, + recentTaskPackages = emptyList(), + ) + assertThat(recentShownTasks).containsExactly(secondaryDisplayRunningTask) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_multipleDesktops_appsNotInHotseat() { + setInDesktopMode(true) + val hotseatPackageUsers = listOf(PackageUser(HOTSEAT_PACKAGE_1, USER_HANDLE_1)) + val defaultDisplayRunningTask = + createTask(id = 1, RUNNING_APP_PACKAGE_1, localUserHandle = USER_HANDLE_1) + val secondaryDisplayRunningTask = + createTask(id = 2, RUNNING_APP_PACKAGE_2, localUserHandle = USER_HANDLE_1) + val runningTasks = + listOf( + PerDisplayRunningApps(listOf(defaultDisplayRunningTask), DEFAULT_DISPLAY), + PerDisplayRunningApps(listOf(secondaryDisplayRunningTask), DEFAULT_DISPLAY + 1), + ) + prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers = hotseatPackageUsers, + runningTasks = runningTasks, + recentTaskPackages = emptyList(), + ) + assertThat(recentShownTasks) + .containsExactly(defaultDisplayRunningTask, secondaryDisplayRunningTask) + assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2)) + } + + @Test + fun onRecentTasksChanged_inDesktopMode_multipleDesktops_multipleAppInstances() { + setInDesktopMode(true) + val hotseatPackageUsers = listOf(PackageUser(HOTSEAT_PACKAGE_1, USER_HANDLE_1)) + val defaultDisplayRunningTask1 = + createTask(id = 1, HOTSEAT_PACKAGE_1, localUserHandle = USER_HANDLE_1) + val defaultDisplayRunningTask2 = + createTask(id = 2, RUNNING_APP_PACKAGE_1, localUserHandle = USER_HANDLE_1) + + val secondaryDisplayRunningTask1 = + createTask(id = 3, RUNNING_APP_PACKAGE_1, localUserHandle = USER_HANDLE_1) + val secondaryDisplayRunningTask2 = + createTask(id = 4, HOTSEAT_PACKAGE_1, localUserHandle = USER_HANDLE_1) + + val runningTasks = + listOf( + PerDisplayRunningApps( + listOf(defaultDisplayRunningTask1, defaultDisplayRunningTask2), + DEFAULT_DISPLAY, + ), + PerDisplayRunningApps( + listOf(secondaryDisplayRunningTask1, secondaryDisplayRunningTask2), + DEFAULT_DISPLAY + 1, + ), + ) + prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers = hotseatPackageUsers, + runningTasks = runningTasks, + recentTaskPackages = emptyList(), + ) + + assertThat(recentShownTasks).hasSize(1) + assertThat(recentShownTasks) + .containsAnyOf(defaultDisplayRunningTask2, secondaryDisplayRunningTask1) + + assertThat(recentAppsController.runningTaskIds) + .containsExactlyElementsIn(listOf(1, 2, 3, 4)) + } + + @Test + fun hasSingleTask_noTargetPackage_returnsFalse() { + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1), + ) + assertThat(recentAppsController.hasSingleTask(ItemInfo())).isFalse() + } + + @Test + fun hasSingleTask_noRecentTasks_returnsFalse() { + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = emptyList(), + ) + val itemInfo = createItemInfo(RECENT_PACKAGE_1) + assertThat(recentAppsController.hasSingleTask(itemInfo)).isFalse() + } + + @Test + fun hasSingleTask_noMatchingSingleTask_returnsFalse() { + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1), + ) + val itemInfo = createItemInfo(RECENT_PACKAGE_2) + assertThat(recentAppsController.hasSingleTask(itemInfo)).isFalse() + } + + @Test + fun hasSingleTask_matchingSingleTask_returnsTrue() { + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2), + ) + val itemInfo = createItemInfo(RECENT_PACKAGE_1) + assertThat(recentAppsController.hasSingleTask(itemInfo)).isTrue() + } + + @Test + fun hasSingleTask_matchingSingleTaskDifferentUser_returnsFalse() { + prepareHotseatAndRunningAndRecentApps( + hotseatPackages = emptyList(), + runningTasks = emptyList(), + recentTaskPackages = listOf(RECENT_PACKAGE_1), + ) + // RECENT_PACKAGE_1 is created with myUserHandle in createRecentTasksFromPackageNames + val itemInfo = createItemInfo(RECENT_PACKAGE_1, USER_HANDLE_1) + assertThat(recentAppsController.hasSingleTask(itemInfo)).isFalse() + } + + private fun prepareHotseatAndRunningAndRecentApps( + hotseatPackages: List, + runningTasks: List, + recentTaskPackages: List, + ): Array { + val hotseatPackageUsers = hotseatPackages.map { PackageUser(it, myUserHandle) } + return prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers, + listOf(PerDisplayRunningApps(runningTasks, DEFAULT_DISPLAY)), + recentTaskPackages, + ) + } + + private fun prepareHotseatAndRunningAndRecentAppsInternal( + hotseatPackageUsers: List, + runningTasks: List, + recentTaskPackages: List, + ): Array { + val hotseatItems = createHotseatItemsFromPackageUsers(hotseatPackageUsers) + recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) + updateRecentTasks(runningTasks, recentTaskPackages) + return recentAppsController.shownHotseatItems.toTypedArray() + } + + private fun updateRecentTasks( + runningTasks: List, + recentTaskPackages: List, + ) { + val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages) + val allTasks = + ArrayList().apply { + runningTasks.forEach { + add(DesktopTask(deskId = 0, it.displayId, ArrayList(it.tasks))) + } + addAll(recentTasks) + } + doAnswer { + val callback: Consumer> = it.getArgument(1) + callback.accept(allTasks) + taskListChangeId + } + .whenever(mockRecentsModel) + .getTasks(any(), any>>()) + recentTasksChangedListener?.onRecentTasksChanged() + } + + private fun createHotseatItemsFromPackageUsers( + packageUsers: List + ): List { + return packageUsers + .map { + createTestAppInfo(packageName = it.packageName, userHandle = it.userHandle).apply { + container = + if (it.packageName.startsWith("predicted")) { + CONTAINER_HOTSEAT_PREDICTION + } else { + CONTAINER_HOTSEAT + } + } + } + .map { it.makeWorkspaceItem(taskbarActivityContext) } + } + + private fun createTestAppInfo( + packageName: String = "testPackageName", + className: String = "testClassName", + userHandle: UserHandle, + ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent()) + + private fun createRecentTasksFromPackageNames(packageNames: List): List { + return packageNames.map { packageName -> + if (packageName.startsWith("split")) { + val splitPackages = packageName.split("_") + SplitTask( + createTask(100, splitPackages[0]), + createTask(101, splitPackages[1]), + SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ 1, + /* rightBottomTaskId = */ 2, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50, + ), + ) + } else { + // Use the number at the end of the test packageName as the id. + val id = 1000 + packageName[packageName.length - 1].code + SingleTask(createTask(id, packageName)) + } + } + } + + private fun createTask( + id: Int, + packageName: String, + isVisible: Boolean = true, + localUserHandle: UserHandle? = null, + ): Task { + return Task( + Task.TaskKey( + id, + WINDOWING_MODE_FREEFORM, + Intent().apply { `package` = packageName }, + ComponentName(packageName, "TestActivity"), + localUserHandle?.identifier ?: myUserHandle.identifier, + 0, + ) + ) + .apply { this.isVisible = isVisible } + } + + private fun setInDesktopMode(inDesktopMode: Boolean) { + whenever(taskbarControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()) + .thenReturn(inDesktopMode) + whenever(taskbarControllers.taskbarDesktopModeController.isInDesktopMode(DEFAULT_DISPLAY)) + .thenReturn(inDesktopMode) + } + + private fun createItemInfo( + packageName: String, + userHandle: UserHandle = myUserHandle, + ): ItemInfo { + val appInfo = AppInfo() + appInfo.intent = Intent().setComponent(ComponentName(packageName, "className")) + appInfo.user = userHandle + return WorkspaceItemInfo(appInfo) + } + + private val GroupTask.packageNames: List + get() = tasks.map { task -> task.key.packageName } + + private companion object { + const val HOTSEAT_PACKAGE_1 = "hotseat1" + const val HOTSEAT_PACKAGE_2 = "hotseat2" + const val PREDICTED_PACKAGE_1 = "predicted1" + const val RUNNING_APP_PACKAGE_1 = "running1" + const val RUNNING_APP_PACKAGE_2 = "running2" + const val RUNNING_APP_PACKAGE_3 = "running3" + const val RECENT_PACKAGE_1 = "recent1" + const val RECENT_PACKAGE_2 = "recent2" + const val RECENT_PACKAGE_3 = "recent3" + const val RECENT_SPLIT_PACKAGES_1 = "split1_split2" + } + + data class PackageUser(val packageName: String, val userHandle: UserHandle) + + data class PerDisplayRunningApps(val tasks: List, val displayId: Int) +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationControllerTest.kt new file mode 100644 index 0000000000..bdc8901ec7 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationControllerTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.animation.AnimatorTestRule +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.BubbleTextView +import com.android.launcher3.BubbleTextView.RunningAppState +import com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED +import com.android.launcher3.BubbleTextView.RunningAppState.NOT_RUNNING +import com.android.launcher3.BubbleTextView.RunningAppState.RUNNING +import com.android.launcher3.model.data.TaskItemInfo +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarRunningAppStateAnimationController.Companion.LINE_ANIM_DURATION +import com.android.launcher3.taskbar.TaskbarRunningAppStateAnimationController.Companion.UNPINNED_APP_LINE_ANIM_DELAY +import com.android.launcher3.util.ActivityContextWrapper +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_APP_RUNNING_STATE_ANIM +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +private const val FRAME_TIME_MS = 16L // Simulates 60 Hz. +private val PINNED_APP = TaskItemInfo(0, TaskbarViewTestUtil.createHotseatWorkspaceItem(0)) +private val UNPINNED_APP = TaskbarViewTestUtil.createRecentTask(1) + +@RunWith(LauncherMultivalentJUnit::class) +class TaskbarRunningAppStateAnimationControllerTest { + + @get:Rule val animatorTestRule = AnimatorTestRule(this) + + private val context = ActivityContextWrapper(getInstrumentation().targetContext) + private val btv = BubbleTextView(context) + private val controller = TaskbarRunningAppStateAnimationController(context) + + @Test + fun updateRunningState_minimizeApp_verifySpringEndState() { + startStateChange(start = RUNNING, end = MINIMIZED) + verifySpringAnimationEnd(MINIMIZED) + } + + @Test + fun updateRunningState_minimizeApp_verifyCancelEndState() { + startStateChange(start = RUNNING, end = MINIMIZED) + runOnMainSync { controller.onDestroy() } + verifyStateSettled(state = MINIMIZED) + } + + @Test + fun updateRunningState_restoreApp_verifySpringEndState() { + startStateChange(start = MINIMIZED, end = RUNNING) + verifySpringAnimationEnd(RUNNING) + } + + @Test + fun updateRunningState_openPinnedApp_verifySpringEndState() { + btv.tag = PINNED_APP + startStateChange(start = NOT_RUNNING, end = RUNNING) + verifySpringAnimationEnd(RUNNING) + } + + @Test + fun updateRunningState_openUnpinnedApp_verifyStartDelay() { + btv.tag = UNPINNED_APP + startStateChange(start = NOT_RUNNING, end = RUNNING) + runOnMainSync { animatorTestRule.advanceTimeBy(UNPINNED_APP_LINE_ANIM_DELAY) } + verifyLineIndicator(state = NOT_RUNNING) + } + + @Test + fun updateRunningState_openUnpinnedApp_verifyEndState() { + btv.tag = UNPINNED_APP + startStateChange(start = NOT_RUNNING, end = RUNNING) + runOnMainSync { + animatorTestRule.advanceTimeBy(UNPINNED_APP_LINE_ANIM_DELAY + LINE_ANIM_DURATION) + } + + verifyStateSettled(state = RUNNING) + } + + @Test + fun updateRunningState_openUnpinnedApp_verifyCancelEndState() { + btv.tag = UNPINNED_APP + startStateChange(start = NOT_RUNNING, end = RUNNING) + runOnMainSync { controller.onDestroy() } + verifyStateSettled(state = RUNNING) + } + + @Test + fun updateRunningState_closeApp_verifyEndState() { + startStateChange(start = RUNNING, end = NOT_RUNNING) + runOnMainSync { animatorTestRule.advanceTimeBy(LINE_ANIM_DURATION) } + verifyStateSettled(state = NOT_RUNNING) + } + + @Test + fun updateRunningState_repeatUpdateDuringAnimation_animationNotCanceled() { + startStateChange(start = MINIMIZED, end = RUNNING) + runOnMainSync { + animatorTestRule.advanceTimeBy(FRAME_TIME_MS) + controller.updateRunningState(btv, RUNNING, animate = false) + } + assertThat(controller.isAnimationRunning(btv)).isTrue() + } + + @Test + fun updateRunningState_minimizedDuringOpen_verifyMinimizedEndState() { + startStateChange(start = NOT_RUNNING, end = RUNNING) + runOnMainSync { controller.updateRunningState(btv, MINIMIZED, animate = true) } + verifySpringAnimationEnd(MINIMIZED) + } + + @Test + fun onDestroy_multipleAnimations_cancelsAll() { + startStateChange(start = RUNNING, end = MINIMIZED) + val btv2 = BubbleTextView(context) + startStateChange(btv = btv2, start = RUNNING, end = MINIMIZED) + + runOnMainSync { controller.onDestroy() } + verifyStateSettled(state = MINIMIZED) + verifyStateSettled(btv = btv2, state = MINIMIZED) + } + + private fun startStateChange( + btv: BubbleTextView = this.btv, + start: RunningAppState, + end: RunningAppState, + ) { + runOnMainSync { + controller.updateRunningState(btv, start, animate = false) + controller.updateRunningState(btv, end, animate = true) + } + verifyLineIndicator(btv, start) + assertThat(controller.isAnimationRunning(btv)).isTrue() + } + + /** Verifies [btv] spring animation ends at [state]. */ + private fun verifySpringAnimationEnd(state: RunningAppState) { + while (controller.isAnimationRunning(btv)) { + runOnMainSync { animatorTestRule.advanceTimeBy(FRAME_TIME_MS) } + } + verifyStateSettled(state = state) + } + + private fun verifyStateSettled(btv: BubbleTextView = this.btv, state: RunningAppState) { + assertThat(controller.isAnimationRunning(btv)).isFalse() + verifyLineIndicator(btv, state) + + val translateYProp = + btv.translateDelegate.getTranslationY(INDEX_TASKBAR_APP_RUNNING_STATE_ANIM) + assertThat(translateYProp.value).isZero() + } + + private fun verifyLineIndicator(btv: BubbleTextView = this.btv, state: RunningAppState) { + controller.run { + assertThat(btv.lineIndicatorWidth).isEqualTo(state.lineWidth) + assertThat(btv.lineIndicatorColor).isEqualTo(state.lineColor) + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt new file mode 100644 index 0000000000..ba53dcdc2b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt @@ -0,0 +1,196 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.rules.SandboxParams +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.quickstep.SystemUiProxy +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR +import com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +class TaskbarScrimViewControllerTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) + val context = + TaskbarWindowSandboxContext.create( + SandboxParams({ + spy(SystemUiProxy(ApplicationProvider.getApplicationContext())) { + doAnswer { backPressed = true }.whenever(it).onBackEvent(anyOrNull()) + } + }) + ) + + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 3) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var scrimViewController: TaskbarScrimViewController + + // Default animation duration. + private val animationDuration = + context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() + + private var backPressed = false + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibleChanged_onlyTaskbarVisible_noScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + scrimViewController.updateStateForSysuiFlags(0, true) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarVisibleWithBubblesExpanded_showsScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + animatorTestRule.advanceTimeBy(animationDuration) + } + + assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA) + } + + @Test + @DisableFlags(FLAG_ENABLE_BUBBLE_BAR) + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarHiddenDuringScrim_hidesScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA) + + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(GONE) + animatorTestRule.advanceTimeBy(animationDuration) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarOnHomeHiddenDuringScrim_hidesScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + taskbarUnitTestRule.activityContext.bubbleControllers!! + .bubbleStashController + .launcherState = BubbleStashController.BubbleLauncherState.HOME + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA) + + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(GONE) + animatorTestRule.advanceTimeBy(animationDuration) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_notificationsOverPinnedTaskbarAndBubbles_noScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags( + SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, + true, + ) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnTaskbarVisibilityChanged_pinnedTaskbarWithBubbleMenu_darkerScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + scrimViewController.updateStateForSysuiFlags( + SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, + true, + ) + } + assertThat(scrimViewController.scrimAlpha).isGreaterThan(BUBBLE_EXPANDED_SCRIM_ALPHA) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testOnTaskbarVisibilityChanged_stashedTaskbarWithBubbles_noScrim() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimAlpha).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testOnClick_scrimShown_performsSystemBack() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimView.isClickable).isTrue() + + getInstrumentation().runOnMainSync { scrimViewController.scrimView.performClick() } + assertThat(backPressed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testOnClick_scrimHidden_notClickable() { + getInstrumentation().runOnMainSync { + scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true) + scrimViewController.onTaskbarVisibilityChanged(VISIBLE) + } + assertThat(scrimViewController.scrimView.isClickable).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt new file mode 100644 index 0000000000..1575e23cac --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt @@ -0,0 +1,779 @@ +/* + * 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.launcher3.taskbar + +import android.animation.AnimatorTestRule +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.Flags +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE +import com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION +import com.android.launcher3.R +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED +import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_DEVICE_LOCKED +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IME +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_APP_AUTO +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SMALL_SCREEN +import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SYSUI +import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION +import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION_FOR_IME +import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION +import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_DURATION +import com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_STASH +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.taskbar.rules.displayControllerSpy +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE +import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.TruthJUnit.assume +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EnableFlags(FLAG_ENABLE_BUBBLE_BAR) +@EmulatedDevices(["pixelTablet2023"]) +class TaskbarStashControllerTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context) + @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var stashController: TaskbarStashController + @InjectController lateinit var viewController: TaskbarViewController + @InjectController lateinit var stashedHandleViewController: StashedHandleViewController + @InjectController lateinit var dragLayerController: TaskbarDragLayerController + @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController + @InjectController lateinit var bubbleBarViewController: BubbleBarViewController + @InjectController lateinit var bubbleStashController: BubbleStashController + + private val desktopVisibilityController: DesktopVisibilityController + get() = DesktopVisibilityController.INSTANCE[context] + + private val activityContext by taskbarUnitTestRule::activityContext + + @After fun cancelTimeoutIfExists() = stashController.cancelTimeoutIfExists() + + @Test + @TaskbarMode(TRANSIENT) + fun testInit_transientMode_stashedInApp() { + assertThat(stashController.isStashedInApp).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testInit_pinnedMode_unstashedInApp() { + assertThat(stashController.isStashedInApp).isFalse() + } + + @Test + @UserSetupMode + @TaskbarMode(PINNED) + fun testInit_userSetupWithPinnedMode_stashedInApp() { + assertThat(stashController.isStashedInApp).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testSetSetupUiVisible_true_stashedInApp() { + getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(true) } + assertThat(stashController.isStashedInApp).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testSetSetupUiVisible_false_unstashedInApp() { + getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(false) } + assertThat(stashController.isStashedInApp).isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_OVERVIEW_ON_CONNECTED_DISPLAYS) + fun testRecreateAsTransient_withoutOverviewOnConnectedDisplays_timeoutStarted() { + context.displayControllerSpy?.setupTaskbarPinningPrefListener(context.displayId) + + testRecreateAsTransient_timeoutStarted() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_OVERVIEW_ON_CONNECTED_DISPLAYS) + fun testRecreateAsTransient_withOverviewOnConnectedDisplay_timeoutStarted() { + context.displayControllerSpy?.let { controller -> + controller.setupTaskbarPinningPrefListener(context.displayId) + controller.infoModifierForDisplay = { + spy(it) { on { it?.isTransientTaskbar } doReturn true } + } + } + + testRecreateAsTransient_timeoutStarted() + } + + private fun testRecreateAsTransient_timeoutStarted() { + var isPinned by TASKBAR_PINNING.asProperty(context) + isPinned = true + activityContext.controllers.sharedState?.taskbarWasPinned = true + + isPinned = false + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testSupportsVisualStashing_transientMode_supported() { + assertThat(stashController.supportsVisualStashing()).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testSupportsVisualStashing_pinnedMode_supported() { + assertThat(stashController.supportsVisualStashing()).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testSupportsVisualStashing_threeButtonsMode_unsupported() { + assertThat(stashController.supportsVisualStashing()).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetStashDuration_transientMode() { + assertThat(stashController.stashDuration).isEqualTo(TRANSIENT_TASKBAR_STASH_DURATION) + } + + @Test + @TaskbarMode(PINNED) + fun testGetStashDuration_pinnedMode() { + assertThat(stashController.stashDuration).isEqualTo(PINNED_TASKBAR_TRANSITION_DURATION) + } + + @Test + @TaskbarMode(PINNED) + fun testIsStashed_pinnedInApp_isUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsStashed_transientInApp_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsStashed_transientNotInApp_isUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + fun testIsStashed_stashedInLauncherState_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsStashed_transientInOverview_isUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testIsStashed_pinnedInOverviewWithIme_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true) + stashController.updateStateForFlag(FLAG_STASHED_IME, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testIsStashed_pinnedTaskbarWithPinnedApp_isStashed() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.updateStateForFlag(FLAG_STASHED_SYSUI, true) // App pinned. + stashController.applyState(0) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + fun testIsInStashedLauncherState_flagUnset_false() { + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, false) + assertThat(stashController.isInStashedLauncherState).isFalse() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testIsInStashedLauncherState_flagSetInThreeButtonsMode_false() { + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true) + assertThat(stashController.isInStashedLauncherState).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testIsInStashedLauncherState_flagSetInPinnedMode_true() { + stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true) + assertThat(stashController.isInStashedLauncherState).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testIsTaskbarVisibleAndNotStashing_pinnedButNotVisible_false() { + getInstrumentation().runOnMainSync { + viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 0f + } + assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testIsTaskbarVisibleAndNotStashing_visibleButStashed_false() { + getInstrumentation().runOnMainSync { + viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f + } + assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testIsTaskbarVisibleAndNotStashing_pinnedAndVisible_true() { + getInstrumentation().runOnMainSync { + viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f + } + assertThat(stashController.isTaskbarVisibleAndNotStashing).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetTouchableHeight_isStashed_stashedHeight() { + assertThat(stashController.touchableHeight).isEqualTo(stashController.stashedHeight) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetTouchableHeight_unstashedTransientMode_heightAndBottomMargin() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false) + stashController.applyState(0) + } + + val expectedHeight = + activityContext.deviceProfile.taskbarProfile.run { height + bottomMargin } + assertThat(stashController.touchableHeight).isEqualTo(expectedHeight) + } + + @Test + @TaskbarMode(PINNED) + fun testGetTouchableHeight_pinnedMode_taskbarHeight() { + assertThat(stashController.touchableHeight) + .isEqualTo(activityContext.deviceProfile.taskbarProfile.height) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetContentHeightToReportToApps_transientMode_stashedHeight() { + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(stashController.stashedHeight) + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testGetContentHeightToReportToApps_threeButtonsMode_taskbarHeight() { + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(activityContext.deviceProfile.taskbarProfile.height) + } + + @Test + @TaskbarMode(PINNED) + fun testGetContentHeightToReportToApps_pinnedMode_taskbarHeight() { + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(activityContext.deviceProfile.taskbarProfile.height) + } + + @Test + @TaskbarMode(PINNED) + @UserSetupMode + fun testGetContentHeightToReportToApps_pinnedInSetupMode_setupWizardInsets() { + stashController.mNavbarHiddenOverrideForTest = false + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(context.resources.getDimensionPixelSize(R.dimen.taskbar_suw_insets)) + stashController.mNavbarHiddenOverrideForTest = null + } + + @Test + @UserSetupMode + fun testGetContentHeightToReportToApps_inExpressiveTheme_setupWizardInsets() { + stashController.mNavbarHiddenOverrideForTest = true + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(stashController.stashedHeight) + stashController.mNavbarHiddenOverrideForTest = null + } + + @Test + @TaskbarMode(PINNED) + fun testGetContentHeightToReportToApps_pinnedModeButFolded_stashedHeight() { + getInstrumentation().runOnMainSync { + stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f + stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true) + } + assertThat(stashController.contentHeightToReportToApps) + .isEqualTo(stashController.stashedHeight) + } + + @Test + @TaskbarMode(PINNED) + fun testGetContentHeightToReportToApps_homeDisabledWhenFolded_zeroHeight() { + getInstrumentation().runOnMainSync { + stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f + stashedHandleViewController.setIsHomeButtonDisabled(true) + stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true) + } + assertThat(stashController.contentHeightToReportToApps).isEqualTo(0) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testGetTappableHeightToReportToApps_transientMode_zeroHeight() { + assertThat(stashController.tappableHeightToReportToApps).isEqualTo(0) + } + + @Test + @TaskbarMode(PINNED) + fun testGetTappableHeightToReportToApps_pinnedMode_taskbarHeight() { + assertThat(stashController.tappableHeightToReportToApps) + .isEqualTo(activityContext.deviceProfile.taskbarProfile.height) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_unstashTaskbar_updatesState() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_runUnstashAnimation_startsTaskbarTimeout() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testUpdateTaskbarTimeout_unPinnedTaskbarInDesktopMode_startsTaskbarTimeout() { + LauncherPrefs.get(context).put(TASKBAR_PINNING_IN_DESKTOP_MODE, false) + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + stashController.updateTaskbarTimeout(false) + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testUpdateTaskbarTimeout_pinnedTaskbarInDesktopMode_shouldNotStartsTaskbarTimeout() { + LauncherPrefs.get(context).put(TASKBAR_PINNING_IN_DESKTOP_MODE, true) + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + stashController.updateTaskbarTimeout(false) + assertThat(stashController.timeoutAlarm.alarmPending()).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun shouldAllowTaskbarToAutoStash_transientTaskbar() { + assertThat(stashController.shouldAllowTaskbarToAutoStash()).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun toggleTaskbarStash_autoStashedDesktopModeTaskbar() { + LauncherPrefs.get(context).put(TASKBAR_PINNING_IN_DESKTOP_MODE, false) + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + + getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() } + + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_finishTaskbarTimeout_taskbarStashes() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + + getInstrumentation().runOnMainSync { + stashController.timeoutAlarm.finishAlarm() + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_autoHideSuspendedForEdu_remainsUnstashed() { + getInstrumentation().runOnMainSync { + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + + getInstrumentation().runOnMainSync { + autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, true) + stashController.updateAndAnimateTransientTaskbar(true) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithBubbles_bubbleBarUnstashes() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.stashBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(false, true) + } + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithoutBubbles_bubbleBarStashed() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.stashBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(false, false) + } + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithBubbles_bubbleBarStashes() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.showBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(true, true) + } + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithoutBubbles_bubbleBarUnstashed() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleStashController.showBubbleBarImmediate() + stashController.updateAndAnimateTransientTaskbar(true, false) + } + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUpdateAndAnimateTransientTaskbar_bubbleBarExpandedBeforeTimeout_expandedAfterwards() { + getInstrumentation().runOnMainSync { + bubbleBarViewController.setHiddenForBubbles(false) + bubbleBarViewController.animateExpanded(true) + stashController.updateAndAnimateTransientTaskbar(false) + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashController.timeoutAlarm.alarmPending()).isTrue() + + getInstrumentation().runOnMainSync { + stashController.timeoutAlarm.finishAlarm() + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(bubbleBarViewController.isExpanded).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testToggleTaskbarStash_pinnedMode_doesNothing() { + getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testToggleTaskbarStash_transientMode_unstashesTaskbar() { + getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testToggleTaskbarStash_twiceInTransientMode_stashesTaskbar() { + getInstrumentation().runOnMainSync { + stashController.toggleTaskbarStash() + stashController.toggleTaskbarStash() + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testToggleTaskbarStash_notInAppWithTransientMode_doesNothing() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.applyState(0) + stashController.toggleTaskbarStash() + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testAnimateTransientTaskbar_bubblesShownInOverview_stashesTaskbar() { + // Start in Overview. Should unstash Taskbar. + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false) + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true) + stashController.applyState(0) + } + assertThat(stashController.isStashed).isFalse() + + // Expand bubbles. Should stash Taskbar. + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION) + } + assertThat(stashController.isStashed).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testAnimatePinnedTaskbar_imeShown_replacesIconsWithHandle() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + assertThat(viewController.areIconsVisible()).isFalse() + assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testAnimatePinnedTaskbar_imeHidden_replacesHandleWithIcons() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + animatorTestRule.advanceTimeBy(0) + } + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(0, true) + animatorTestRule.advanceTimeBy(0) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse() + assertThat(viewController.areIconsVisible()).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testAnimatePinnedTaskbar_imeHidden_verifyAnimationDuration() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + // Start with IME shown. + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + animatorTestRule.advanceTimeBy(0) + } + + // Hide IME with animation. + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(0, false) + // Fast forward without start delay. + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + // Icons should not be visible yet due to start delay. + assertThat(viewController.areIconsVisible()).isFalse() + + // Advance by start delay retroactively. Animation should complete. + getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(stashController.taskbarStashStartDelayForIme) + } + assertThat(viewController.areIconsVisible()).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testAnimateThreeButtonsTaskbar_imeShown_hidesIconsAndBg() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + assertThat(viewController.areIconsVisible()).isFalse() + assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(0) + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testAnimateThreeButtonsTaskbar_imeHidden_showsIconsAndBg() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, false) + animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME) + } + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(0, false) + animatorTestRule.advanceTimeBy( + TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme + ) + } + assertThat(viewController.areIconsVisible()).isTrue() + assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(1) + } + + @Test + @TaskbarMode(PINNED) + fun testSetSystemGestureInProgress_whileImeShown_unstashesTaskbar() { + assume().that(activityContext.isHardwareKeyboard).isFalse() + + getInstrumentation().runOnMainSync { + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + animatorTestRule.advanceTimeBy(0) + } + + getInstrumentation().runOnMainSync { + stashController.setSystemGestureInProgress(true) + animatorTestRule.advanceTimeBy( + TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme + ) + } + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testSysuiStateImeShowingInApp_hardwareKeyboardWithPinnedMode_notStashedForIme() { + assume().that(activityContext.isHardwareKeyboard).isTrue() + + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, true) + stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_VISIBLE, true) + } + + assertThat(stashController.isStashed).isFalse() + } + + @Test + @TaskbarMode(PINNED) + fun testUnlockTransition_pinnedMode_fadesOutHandle() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true) + stashController.applyState(0) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue() + + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false) + stashController.applyState() + animatorTestRule.advanceTimeBy(stashController.stashDuration) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testUnlockTransition_transientMode_fadesOutHandleEarly() { + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_IN_APP, false) + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true) + stashController.applyState(0) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue() + + getInstrumentation().runOnMainSync { + stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false) + stashController.applyState() + // Time it takes for just the handle to hide (full stash animation is longer). + animatorTestRule.advanceTimeBy(TRANSIENT_TASKBAR_STASH_ALPHA_DURATION) + } + assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse() + } +} + +private fun TaskbarStashController.updateStateForFlag(flag: Int, value: Boolean) { + updateStateForFlag(flag.toLong(), value) +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt deleted file mode 100644 index a999e7f7de..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.launcher3.taskbar - -import android.app.Instrumentation -import android.app.PendingIntent -import android.content.IIntentSender -import android.content.Intent -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ServiceTestRule -import com.android.launcher3.LauncherAppState -import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks -import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR -import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric -import com.android.quickstep.AllAppsActionManager -import com.android.quickstep.TouchInteractionService -import com.android.quickstep.TouchInteractionService.TISBinder -import org.junit.Assume.assumeTrue -import org.junit.rules.MethodRule -import org.junit.runners.model.FrameworkMethod -import org.junit.runners.model.Statement - -/** - * Manages the Taskbar lifecycle for unit tests. - * - * See [InjectController] for grabbing controller(s) under test with minimal boilerplate. - * - * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is - * that code that is executed on the main thread in production should also happen on that thread - * when tested. - * - * `@UiThreadTest` is a simple way to run an entire test body on the main thread. But if a test - * executes code that appends message(s) to the main thread's `MessageQueue`, the annotation will - * prevent those messages from being processed until after the test body finishes. - * - * To test pending messages, instead use something like [Instrumentation.runOnMainSync] to perform - * only sections of the test body on the main thread synchronously: - * ``` - * @Test - * fun example() { - * instrumentation.runOnMainSync { doWorkThatPostsMessage() } - * // Second lambda will not execute until message is processed. - * instrumentation.runOnMainSync { verifyMessageResults() } - * } - * ``` - */ -class TaskbarUnitTestRule : MethodRule { - private val instrumentation = InstrumentationRegistry.getInstrumentation() - private val serviceTestRule = ServiceTestRule() - - private lateinit var taskbarManager: TaskbarManager - private lateinit var target: Any - - val activityContext: TaskbarActivityContext - get() { - return taskbarManager.currentActivityContext - ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.") - } - - override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement { - return object : Statement() { - override fun evaluate() { - this@TaskbarUnitTestRule.target = target - - val context = instrumentation.targetContext - instrumentation.runOnMainSync { - assumeTrue( - LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent - ) - } - - // Check for existing Taskbar instance from Launcher process. - val launcherTaskbarManager: TaskbarManager? = - if (!isRunningInRobolectric) { - try { - val tisBinder = - serviceTestRule.bindService( - Intent(context, TouchInteractionService::class.java) - ) as? TISBinder - tisBinder?.taskbarManager - } catch (_: Exception) { - null - } - } else { - null - } - - instrumentation.runOnMainSync { - taskbarManager = - TaskbarManager( - context, - AllAppsActionManager(context, UI_HELPER_EXECUTOR) { - PendingIntent(IIntentSender.Default()) - }, - object : TaskbarNavButtonCallbacks {}, - ) - } - - try { - // Replace Launcher Taskbar window with test instance. - instrumentation.runOnMainSync { - launcherTaskbarManager?.removeTaskbarRootViewFromWindow() - taskbarManager.onUserUnlocked() // Required to complete initialization. - } - - injectControllers() - base.evaluate() - } finally { - // Revert Taskbar window. - instrumentation.runOnMainSync { - taskbarManager.destroy() - launcherTaskbarManager?.addTaskbarRootViewToWindow() - } - } - } - } - } - - /** Simulates Taskbar recreation lifecycle. */ - fun recreateTaskbar() { - taskbarManager.recreateTaskbar() - injectControllers() - } - - private fun injectControllers() { - val controllers = activityContext.controllers - val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type } - target.javaClass.fields - .filter { it.isAnnotationPresent(InjectController::class.java) } - .forEach { - it.set( - target, - controllerFieldsByType[it.type]?.get(controllers) - ?: throw NoSuchElementException("Failed to find controller for ${it.type}"), - ) - } - } - - /** - * Annotates test controller fields to inject the corresponding controllers from the current - * [TaskbarControllers] instance. - * - * Controllers are injected during test setup and upon calling [recreateTaskbar]. - * - * Multiple controllers can be injected if needed. - */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FIELD) - annotation class InjectController -} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt new file mode 100644 index 0000000000..b13eafe5bb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt @@ -0,0 +1,236 @@ +/* + * 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.launcher3.taskbar + +import android.view.View +import com.android.launcher3.taskbar.TaskbarViewController.DIVIDER_VIEW_POSITION_OFFSET +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +/** + * Legend for the comments below: + * ``` + * A: All Apps Button + * H: Hotseat item + * |: Divider + * R: Recent item + * ``` + * + * The comments are formatted in two lines: + * ``` + * // Items in taskbar, e.g. A | HHHHHH + * // Index of items relative to Hotseat: -1 -.5 012345 + * ``` + */ +class TaskbarViewControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var taskbarViewController: TaskbarViewController + + @Test + fun testGetPositionInHotseat_allAppsButton_nonRtl() { + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ 6, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ true, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [>A<] | [HHHHHH] + // -1 -.5 012345 + assertThat(position).isEqualTo(-1) + } + + @Test + fun testGetPositionInHotseat_allAppsButton_rtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ true, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [HHHHHH] | [>A<] + // 012345 5.5 6 + assertThat(position).isEqualTo(numShownHotseatIcons) + } + + @Test + fun testGetPositionInHotseat_dividerView_notForRecents_nonRtl() { + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ 6, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [A] >|< [HHHHHH] + // -1 -.5 012345 + assertThat(position).isEqualTo(-DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_dividerView_forRecents_nonRtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ true, + /* recentTaskIndex = */ -1, + ) + // [A] [HHHHHH] >|< [RR] + // -1 012345 5.5 67 + assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_dividerView_notForRecents_rtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ -1, + ) + // [HHHHHH] >|< [A] + // 012345 5.5 6 + assertThat(position).isEqualTo(numShownHotseatIcons - DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_dividerView_forRecents_rtl() { + val numShownHotseatIcons = 6 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ true, + /* isDividerForRecents = */ true, + /* recentTaskIndex = */ -1, + ) + // [HHHHHH][A] >|< [RR] + // 012345 6 6.5 78 + assertThat(position).isEqualTo(numShownHotseatIcons + DIVIDER_VIEW_POSITION_OFFSET) + } + + @Test + fun testGetPositionInHotseat_recentTasks_firstRecentIndex_nonRtl() { + val numShownHotseatIcons = 6 + val recentTaskIndex = 0 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ false, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ recentTaskIndex, + ) + // [A][HHHHHH] | [>RR<] + // -1 012345 5.5 6 7 + assertThat(position).isEqualTo(numShownHotseatIcons + recentTaskIndex) + } + + @Test + fun testGetPositionInHotseat_recentTasks_firstRecentIndex_rtl() { + val numShownHotseatIcons = 6 + val recentTaskIndex = 0 + val position = + taskbarViewController.getPositionInHotseat( + /* numShownHotseatIcons = */ numShownHotseatIcons, + /* child = */ View(context), + /* isRtl = */ true, + /* isAllAppsButton = */ false, + /* isTaskbarDividerView = */ false, + /* isDividerForRecents = */ false, + /* recentTaskIndex = */ recentTaskIndex, + ) + // [HHHHHH][A] | [>RR<] + // 012345 6 6.5 7 8 + assertThat(position).isEqualTo(numShownHotseatIcons + 1 + recentTaskIndex) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt new file mode 100644 index 0000000000..24ed81f4fa --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt @@ -0,0 +1,166 @@ +/* + * 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.launcher3.taskbar + +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.rules.TaskbarDeviceEmulationRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric +import com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION +import org.junit.Before +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 + +@RunWith(ParameterizedAndroidJunit4::class) +class TaskbarViewTest(deviceName: String, flags: FlagsParameterization) { + + companion object { + @JvmStatic + @Parameters(name = "{0},{1}") + fun getParams(): List> { + val devices = + if (isRunningInRobolectric) { + listOf("pixelFoldable2023", "pixelTablet2023") + } else { + listOf("onDevice") // Unused. + } + val flags = allCombinationsOf(FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION) + return devices.flatMap { d -> flags.map { f -> arrayOf(d, f) } } // Cartesian product. + } + } + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule(flags) + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val deviceEmulationRule = TaskbarDeviceEmulationRule(context, deviceName) + @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + private lateinit var taskbarView: TaskbarView + + @Before + fun obtainView() { + taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + } + + @Test + fun testUpdateItems_noItems_hasOnlyAllApps() { + runOnMainSync { taskbarView.updateItems(emptyArray(), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS) + } + + @Test + fun testUpdateItems_hotseatItems_hasDividerBetweenAllAppsAndHotseat() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlWithHotseatItems_hasDividerBetweenHotseatAndAllApps() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_withNullHotseatItem_filtersNullItem() { + runOnMainSync { + taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList()) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlWithNullHotseatItem_filtersNullItem() { + runOnMainSync { + taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList()) + } + assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_recentsItems_hasDividerBetweenAllAppsAndRecents() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, *RECENT * 4) + } + + @Test + fun testUpdateItems_hotseatItemsAndRecents_hasDividerBetweenHotseatAndRecents() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 3, DIVIDER, *RECENT * 2) + } + + @Test + fun testUpdateItems_addHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 2, DIVIDER, RECENT) + } + + @Test + fun testUpdateItems_removeHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } + + @Test + fun testUpdateItems_addRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, *RECENT * 2) + } + + @Test + fun testUpdateItems_removeRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } + + @Test + fun testUpdateItem_addHotseatItemAfterRecentsItem_hotseatItemBeforeDivider() { + runOnMainSync { + taskbarView.updateItems(emptyArray(), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt new file mode 100644 index 0000000000..9cd09dc76d --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt @@ -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. + */ + +package com.android.launcher3.taskbar + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Bitmap.createBitmap +import android.os.Process +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.icons.BitmapInfo +import com.android.launcher3.model.data.AppPairInfo +import com.android.launcher3.model.data.FolderInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat + +/** Common utilities for testing [TaskbarView]. */ +object TaskbarViewTestUtil { + + /** Begins an assertion about a [TaskbarView]. */ + fun assertThat(view: TaskbarView): TaskbarViewSubject { + return assertAbout(::TaskbarViewSubject).that(view) + } + + /** Creates an array of fake hotseat items. */ + fun createHotseatItems(size: Int): Array { + return Array(size) { createHotseatWorkspaceItem(it) } + } + + fun createHotseatWorkspaceItem(id: Int = 0): WorkspaceItemInfo { + return createTestWorkspaceItem( + id, + "Test App $id", + testIntent(id), + Process.myUserHandle(), + CONTAINER_HOTSEAT, + ) + } + + // Helper to create a test WorkspaceItemInfo + fun createTestWorkspaceItem( + id: Int, + title: String, + intent: Intent, + user: android.os.UserHandle, + container: Int, + ): WorkspaceItemInfo { + val item = WorkspaceItemInfo() + item.id = id + item.title = title + item.intent = intent + item.user = user + item.container = container + // Create a placeholder icon so that the test doesn't try to load a high-res icon. + item.bitmap = BitmapInfo.fromBitmap(createBitmap(1, 1, Bitmap.Config.ALPHA_8)) + return item + } + + fun createHotseatAppPairsItem(): AppPairInfo { + return AppPairInfo().apply { + add(createHotseatWorkspaceItem(1)) + add(createHotseatWorkspaceItem(2)) + } + } + + fun createHotseatFolderItem(): FolderInfo { + return FolderInfo().apply { + title = "Test Folder" + add(createHotseatWorkspaceItem(1)) + add(createHotseatWorkspaceItem(2)) + add(createHotseatWorkspaceItem(3)) + } + } + + /** Creates a list of fake recent tasks. */ + fun createRecents(size: Int): List { + return List(size) { createRecentTask(it) } + } + + fun createRecentTask(id: Int = 0): GroupTask { + return SingleTask( + Task().apply { + key = + TaskKey( + id, + 5, + testIntent(id), + testComponent(id), + Process.myUserHandle().identifier, + System.currentTimeMillis(), + ) + } + ) + } +} + +/** A `Truth` [Subject] with extensions for verifying [TaskbarView]. */ +class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: TaskbarView?) : + Subject(failureMetadata, view) { + + /** Verifies that the types of icons match [expectedTypes] in order. */ + fun hasIconTypes(vararg expectedTypes: TaskbarIconType) { + val actualTypes = + view?.iconViews?.map { + when (it) { + view.allAppsButtonContainer -> ALL_APPS + view.taskbarDividerViewContainer -> DIVIDER + view.taskbarOverflowView -> OVERFLOW + else -> + when (it.tag) { + is ItemInfo -> HOTSEAT + is GroupTask -> RECENT + else -> throw IllegalStateException("Unknown type for $it") + } + } + } + assertThat(actualTypes).containsExactly(*expectedTypes).inOrder() + } + + /** Verifies that recents from [startIndex] have IDs that match [expectedIds] in order. */ + fun hasRecentsOrder(startIndex: Int, expectedIds: List) { + val actualIds = + view?.iconViews?.slice(startIndex.. task.key.id } + } + assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder() + } +} + +/** Types of icons in the [TaskbarView]. */ +enum class TaskbarIconType { + ALL_APPS, + DIVIDER, + HOTSEAT, + RECENT, + OVERFLOW; + + operator fun times(size: Int) = Array(size) { this } +} + +private const val TEST_PACKAGE = "com.android.launcher3.taskbar" +private val testComponent = { i: Int -> ComponentName(TEST_PACKAGE, "Activity $i") } +private val testIntent = { i: Int -> + Intent().apply { + `package` = TEST_PACKAGE + component = testComponent(i) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt new file mode 100644 index 0000000000..a223624c5f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt @@ -0,0 +1,363 @@ +/* + * 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.launcher3.taskbar + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.View +import com.android.launcher3.R +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_OVERFLOW +import com.android.window.flags.Flags.FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.whenever + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +@EnableFlags(FLAG_ENABLE_TASKBAR_OVERFLOW, FLAG_ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION) +class TaskbarViewWithLayoutTransitionTest { + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + private lateinit var taskbarView: TaskbarView + + private val iconViews: Array + get() = taskbarView.iconViews + + private val desktopVisibilityController: DesktopVisibilityController + get() = DesktopVisibilityController.INSTANCE[context] + + private val maxShownRecents: Int + get() = taskbarView.maxNumIconViews - 2 // Account for All Apps and Divider. + + @Before + fun obtainView() { + taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_hotseatItems_hasDividerBetweenHotseatAndAllApps() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_recentsItems_hasDividerBetweenRecentsAndAllApps() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) } + assertThat(taskbarView).hasIconTypes(*RECENT * 4, DIVIDER, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_recentsItems_recentsAreReversed() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) } + assertThat(taskbarView).hasRecentsOrder(startIndex = 0, expectedIds = listOf(3, 2, 1, 0)) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_hotseatItemsAndRecents_hasDividerBetweenRecentsAndHotseat() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) } + assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, *HOTSEAT * 3, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addHotseatItemWithoutRecents_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), emptyList()) + taskbarView.updateItems(createHotseatItems(2), emptyList()) + } + assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addHotseatItemWithRecents_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, *HOTSEAT * 2, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + } + assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, HOTSEAT, ALL_APPS) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS) + } + + @Test + fun testUpdateItems_addRecentsItem_viewAddedOnRight() { + runOnMainSync { + taskbarView.updateItems(emptyArray(), createRecents(1)) + val prevIconViews = iconViews + + val newRecents = createRecents(2) + taskbarView.updateItems(emptyArray(), newRecents) + + assertThat(taskbarView).hasRecentsOrder(startIndex = 2, expectedIds = listOf(0, 1)) + assertThat(iconViews[2]).isSameInstanceAs(prevIconViews[2]) + assertThat(iconViews.last() in prevIconViews).isFalse() + } + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_addRecentsItem_viewAddedOnLeft() { + runOnMainSync { + taskbarView.updateItems(emptyArray(), createRecents(1)) + val prevIconViews = iconViews + + val newRecents = createRecents(2) + taskbarView.updateItems(emptyArray(), newRecents) + + assertThat(taskbarView).hasRecentsOrder(startIndex = 0, expectedIds = listOf(1, 0)) + assertThat(iconViews[1]).isSameInstanceAs(prevIconViews.first()) + assertThat(iconViews.first() in prevIconViews).isFalse() + } + } + + @Test + fun testUpdateItems_removeFirstRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[2] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.first()) + + taskbarView.updateItems(emptyArray(), listOf(recents.last())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + fun testUpdateItems_removeLastRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[3] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.last()) + + taskbarView.updateItems(emptyArray(), listOf(recents.first())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeFirstRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[1] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.first()) + + taskbarView.updateItems(emptyArray(), listOf(recents.last())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_removeLastRecentsItem_correspondingViewRemoved() { + runOnMainSync { + val recents = createRecents(2) + taskbarView.updateItems(emptyArray(), recents) + + val expectedViewToRemove = iconViews[0] + assertThat(expectedViewToRemove.tag).isEqualTo(recents.last()) + + taskbarView.updateItems(emptyArray(), listOf(recents.first())) + assertThat(expectedViewToRemove in iconViews).isFalse() + } + } + + @Test + fun testUpdateItems_desktopMode_hotseatItem_noDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(createHotseatItems(1), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT) + } + + @Test + fun testUpdateItems_desktopMode_hotseatItem_noDividerAfterDesktopModeChange() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(false) + runOnMainSync { taskbarView.updateItems(createHotseatItems(1), emptyList()) } + + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlAndDesktopMode_hotseatItem_noDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(createHotseatItems(1), emptyList()) } + assertThat(taskbarView).hasIconTypes(HOTSEAT, ALL_APPS) + } + + @Test + fun testUpdateItems_desktopMode_recentItem_hasDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(1)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, RECENT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlAndDesktopMode_recentItem_hasDivider() { + whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true) + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(1)) } + assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_maxRecents_noOverflow() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(maxShownRecents)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, *RECENT * maxShownRecents) + } + + @Test + fun testUpdateItems_moreThanMaxRecents_overflowShownBeforeRecents() { + val recentsSize = maxShownRecents + 2 + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(recentsSize)) } + + val expectedNumRecents = RECENT * getExpectedNumRecentsWithOverflow() + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, OVERFLOW, *expectedNumRecents) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_moreThanMaxRecents_overflowShownAfterRecents() { + val recentsSize = maxShownRecents + 2 + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(recentsSize)) } + + val expectedRecents = RECENT * getExpectedNumRecentsWithOverflow() + assertThat(taskbarView).hasIconTypes(*expectedRecents, OVERFLOW, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_moreThanMaxRecentsWithHotseat_fewerRecentsShown() { + val hotseatSize = 4 + val recentsSize = maxShownRecents + 2 + runOnMainSync { + taskbarView.updateItems(createHotseatItems(hotseatSize), createRecents(recentsSize)) + } + + val expectedRecents = RECENT * getExpectedNumRecentsWithOverflow(hotseatSize) + assertThat(taskbarView) + .hasIconTypes(ALL_APPS, *HOTSEAT * hotseatSize, DIVIDER, OVERFLOW, *expectedRecents) + } + + @Test + @ForceRtl + fun testUpdateItems_rtl_moreThanMaxRecentsWithHotseat_fewerRecentsShown() { + val hotseatSize = 4 + val recentsSize = maxShownRecents + 2 + runOnMainSync { + taskbarView.updateItems(createHotseatItems(hotseatSize), createRecents(recentsSize)) + } + + val expectedRecents = RECENT * getExpectedNumRecentsWithOverflow(hotseatSize) + assertThat(taskbarView) + .hasIconTypes(*expectedRecents, OVERFLOW, DIVIDER, *HOTSEAT * hotseatSize, ALL_APPS) + } + + @Test + fun testUpdateItems_moreThanMaxRecents_verifyShownRecentsOrder() { + val recentsSize = maxShownRecents + 2 + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(recentsSize)) } + + val expectedNumRecents = getExpectedNumRecentsWithOverflow() + assertThat(taskbarView) + .hasRecentsOrder( + startIndex = iconViews.size - expectedNumRecents, + expectedIds = ((recentsSize - expectedNumRecents)..() + + @get:Rule(order = 0) val mockitoRule: MockitoRule = MockitoJUnit.rule() + @get:Rule(order = 1) val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + + private lateinit var bubbleBarSwipeController: BubbleBarSwipeController + + @Mock private lateinit var bubbleBarController: BubbleBarController + @Mock private lateinit var bubbleBarViewController: BubbleBarViewController + @Mock private lateinit var bubbleStashController: BubbleStashController + @Mock private lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController + @Mock private lateinit var bubbleDragController: BubbleDragController + @Mock private lateinit var bubbleDismissController: BubbleDismissController + @Mock private lateinit var bubbleBarPinController: BubbleBarPinController + @Mock private lateinit var bubblePinController: BubblePinController + @Mock private lateinit var dragToBubbleController: DragToBubbleController + @Mock private lateinit var bubbleCreator: BubbleCreator + + @Before + fun setUp() { + val dimensionProvider = + object : BubbleBarSwipeController.DimensionProvider { + override val unstashThreshold: Int + get() = UNSTASH_THRESHOLD + + override val maxOverscroll: Int + get() = MAX_OVERSCROLL + } + bubbleBarSwipeController = BubbleBarSwipeController(context, dimensionProvider) + + val bubbleControllers = + BubbleControllers( + bubbleBarController, + bubbleBarViewController, + bubbleStashController, + Optional.of(bubbleStashedHandleViewController), + bubbleDragController, + bubbleDismissController, + bubbleBarPinController, + bubblePinController, + Optional.of(bubbleBarSwipeController), + dragToBubbleController, + bubbleCreator, + ) + + bubbleBarSwipeController.init(bubbleControllers) + } + + // region Test that views have damped translation on swipe + + private fun testViewsHaveDampedTranslationOnSwipe(swipe: Float) { + val isUp = swipe < 0 + val damped = OverScroll.dampedScroll(abs(swipe), MAX_OVERSCROLL).toFloat() + val dampedTranslation = if (isUp) -damped else damped + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(swipe) + } + verify(bubbleStashedHandleViewController).setTranslationYForSwipe(dampedTranslation) + verify(bubbleBarViewController).setTranslationYForSwipe(dampedTranslation) + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_viewsHaveDampedTranslation() { + setUpStashedBar() + testViewsHaveDampedTranslationOnSwipe(UP_BELOW_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() { + setUpStashedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_collapsedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() { + setUpCollapsedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH) + } + + // endregion + + // region Test that translation on views is reset on finish + + private fun testViewsTranslationResetOnFinish(swipe: Float) { + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(swipe) + bubbleBarSwipeController.finish() + // We use a spring animation. Advance by 5 seconds to give it time to finish + animatorTestRule.advanceTimeBy(5000) + } + val handleSwipeTranslation = argumentCaptor() + val barSwipeTranslation = argumentCaptor() + verify(bubbleStashedHandleViewController, atLeastOnce()) + .setTranslationYForSwipe(handleSwipeTranslation.capture()) + verify(bubbleBarViewController, atLeastOnce()) + .setTranslationYForSwipe(barSwipeTranslation.capture()) + + assertThat(handleSwipeTranslation.firstValue).isNonZero() + assertThat(handleSwipeTranslation.lastValue).isZero() + + assertThat(barSwipeTranslation.firstValue).isNonZero() + assertThat(barSwipeTranslation.lastValue).isZero() + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpStashedBar() + testViewsTranslationResetOnFinish(UP_BELOW_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpStashedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_collapsedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpCollapsedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH) + } + + // endregion + + // region Test swipe interactions on stashed bar + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_doesNotShowBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_isSwipeGestureFalse() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse() + } + + @Test + fun swipeUp_stashedBar_overUnstashThreshold_unstashBubbleBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + } + + @Test + fun swipeUp_stashedBar_overUnstashThreshold_isSwipeGestureTrue() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue() + } + + @Test + fun swipeUp_stashedBar_overUnstashThresholdMultipleTimes_unstashesMultipleTimes() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + verify(bubbleStashController).stashBubbleBar() + + getInstrumentation().runOnMainSync { bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) } + verify(bubbleStashController, times(2)) + .showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + } + + @Test + fun swipeUp_stashedBar_releaseOverUnstashThreshold_expandsBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController, never()).showBubbleBar(expandBubbles = eq(true), any()) + getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() } + verify(bubbleStashController).showBubbleBar(expandBubbles = true, bubbleBarGesture = true) + } + + @Test + fun swipeUp_stashedBar_overUnstashReleaseBelowUnstash_doesNotExpandBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false, bubbleBarGesture = true) + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + bubbleBarSwipeController.finish() + } + verify(bubbleStashController, never()).showBubbleBar(expandBubbles = eq(true), any()) + } + + @Test + fun swipeDown_stashedBar_swipeIgnored() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(DOWN) + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + // endregion + + // region Test swipe interactions on expanded bar + + @Test + fun swipe_expandedBar_swipeIgnored() { + setUpExpandedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + bubbleBarSwipeController.swipeTo(DOWN) + bubbleBarSwipeController.finish() + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + // endregion + + // region Test swipe interactions on collapsed bar + + @Test + fun swipeUp_collapsedBar_doesNotShowBarDuringDrag() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeUp_collapsedBar_belowUnstashThreshold_isSwipeGestureFalse() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse() + } + + @Test + fun swipeUp_collapsedBar_overUnstashThreshold_isSwipeGestureTrue() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue() + } + + @Test + fun swipeUp_collapsedBar_finishOverUnstashThreshold_expandsBar() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + bubbleBarSwipeController.finish() + } + verify(bubbleStashController).showBubbleBar(expandBubbles = true, bubbleBarGesture = true) + } + + @Test + fun swipeUp_collapsedBar_finishBelowUnstashThreshold_doesNotExpandBar() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + bubbleBarSwipeController.finish() + } + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeDown_collapsedBar_swipeIgnored() { + setUpCollapsedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(DOWN) + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + verify(bubbleStashController, never()).stashBubbleBar() + } + + // endregion + + private fun setUpStashedBar() { + whenever(bubbleStashController.isStashed).thenReturn(true) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + } + + private fun setUpCollapsedBar() { + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + } + + private fun setUpExpandedBar() { + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true) + whenever(bubbleBarViewController.isExpanded).thenReturn(true) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt new file mode 100644 index 0000000000..4ae887718b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt @@ -0,0 +1,97 @@ +/* + * 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.launcher3.taskbar.bubbles + +import android.content.Context +import android.graphics.Color +import android.graphics.Path +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import androidx.core.graphics.drawable.toBitmap +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.android.wm.shell.shared.bubbles.BubbleInfo +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleViewTest { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleView: BubbleView + private lateinit var overflowView: BubbleView + private lateinit var bubble: BubbleBarBubble + + @Test + fun hasUnseenContent_bubble() { + setupBubbleViews() + assertThat(bubbleView.hasUnseenContent()).isTrue() + + bubbleView.markSeen() + assertThat(bubbleView.hasUnseenContent()).isFalse() + } + + @Test + fun hasUnseenContent_overflow() { + setupBubbleViews() + assertThat(overflowView.hasUnseenContent()).isFalse() + } + + private fun setupBubbleViews() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val inflater = LayoutInflater.from(context) + + val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) + overflowView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView + overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) + + val bubbleInfo = + BubbleInfo( + "key", + 0, + null, + null, + 0, + context.packageName, + null, + null, + false, + true, + null, + ) + bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView + bubble = + BubbleBarBubble( + bubbleInfo, + bubbleView, + bitmap, + bitmap, + Color.WHITE, + Path(), + "", + null, + ) + bubbleView.setBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/DragToBubbleControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/DragToBubbleControllerTest.kt new file mode 100644 index 0000000000..17283c37ee --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/DragToBubbleControllerTest.kt @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Insets +import android.graphics.Rect +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.DropTarget +import com.android.launcher3.DropTarget.DragObject +import com.android.launcher3.dragndrop.DragOptions +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener +import com.android.quickstep.SystemUiProxy +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.DeviceConfig +import com.android.wm.shell.shared.bubbles.DragZoneFactory +import com.android.wm.shell.shared.bubbles.DragZoneFactory.BubbleBarPropertiesProvider +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode +import com.android.wm.shell.shared.bubbles.DropTargetView +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +/** Unit tests for [DragToBubbleControllerTest]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DragToBubbleControllerTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private val context = getApplicationContext() + private val container = FrameLayout(context) + private val bubbleBarViewController: BubbleBarViewController = mock() + private val systemUiProxy: SystemUiProxy = mock() + private val bubbleBarLocationListener: BubbleBarLocationListener = mock() + private val bubbleBarPropertiesProvider = FakeBubbleBarPropertiesProvider() + private val testDragZonesFactory = createTestDragZoneFactory() + private val dragObject = DragObject(context) + private lateinit var dragToBubbleController: DragToBubbleController + + private val dropTargetView: DropTargetView + get() = dragToBubbleController.dropTargetManager.dropTargetView + + private val secondDropTargetView: DropTargetView? + get() = dragToBubbleController.dropTargetManager.secondDropTargetView + + private val bubbleBarLeftDropTarget: DropTarget + get() = dragToBubbleController.bubbleBarLeftDropTarget + + private val bubbleBarRightDropTarget: DropTarget + get() = dragToBubbleController.bubbleBarRightDropTarget + + private val leftDropTargetRect: Rect + get() = testDragZonesFactory.getBubbleBarDropRect(isLeftSide = true) + + private val rightDropTargetRect: Rect + get() = testDragZonesFactory.getBubbleBarDropRect(isLeftSide = false) + + @Before + fun setUp() { + prepareBubbleBarViewController() + dragToBubbleController = DragToBubbleController(context, container) + dragToBubbleController.init( + bubbleBarViewController, + bubbleBarPropertiesProvider, + bubbleBarLocationListener, + systemUiProxy, + ) + dragToBubbleController.dragZoneFactory = testDragZonesFactory + } + + @Test + fun dragStarted_noBubbleBar_dropZonesAdded() { + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT_NO_BUBBLE_BAR) + assertThat(dropTargetView.parent).isEqualTo(container) + assertThat(secondDropTargetView!!.parent).isEqualTo(container) + assertThat(dropTargetView.alpha).isEqualTo(0f) + assertThat(secondDropTargetView!!.alpha).isEqualTo(0f) + assertThat(dragToBubbleController.isItemDropHandled).isFalse() + } + + @Test + fun dragStarted_hasBubbleBar_dropZonesAdded() { + prepareBubbleBarViewController(hasBubbles = true) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT) + assertThat(dropTargetView.parent).isEqualTo(container) + assertThat(dropTargetView.alpha).isEqualTo(0f) + assertThat(secondDropTargetView).isNull() + } + + @Test + fun dragEnded_allViewsRemoved() { + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragToBubbleController.onDragEnd() + animatorTestRule.advanceTimeBy(250) + } + + assertThat(container.childCount).isEqualTo(0) + } + + @Test + fun draggedToTheRightDropZone_noBubbles_dropTargetViewsShown() { + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(1f) + assertThat(secondDropTargetView!!.alpha).isEqualTo(1f) + verify(bubbleBarViewController, never()).animateBubbleBarLocation(any()) + } + + @Test + fun draggedToTheRightDropZone_hasBubblesOnTheRight_dropTargetViewShown() { + prepareBubbleBarViewController(hasBubbles = true) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(1f) + assertThat(secondDropTargetView).isNull() + } + + @Test + fun draggedToTheRightDropZone_hasBubblesOnTheRight_bubbleBarLocationChangeNotRequested() { + prepareBubbleBarViewController(hasBubbles = true) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) + animatorTestRule.advanceTimeBy(250) + } + verify(bubbleBarViewController, never()).animateBubbleBarLocation(any()) + } + + @Test + fun draggedToTheLeftDropZone_hasBubblesOnTheRight_bubbleBarLocationChangeRequested() { + prepareBubbleBarViewController( + hasBubbles = true, + bubbleBarLocation = BubbleBarLocation.RIGHT, + ) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + } + verify(bubbleBarViewController).animateBubbleBarLocation(BubbleBarLocation.LEFT) + } + + @Test + fun draggedToTheLeftDropZone_dragEnded_hasBubblesOnTheRight_locationRestored() { + val bubbleBarOriginalLocation = BubbleBarLocation.RIGHT + prepareBubbleBarViewController( + hasBubbles = true, + bubbleBarLocation = bubbleBarOriginalLocation, + ) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + dragObject.updateXY(x = 0, y = 0) + bubbleBarLeftDropTarget.onDragExit(dragObject) + dragToBubbleController.onDragEnd() + } + + verify(bubbleBarViewController).animateBubbleBarLocation(BubbleBarLocation.LEFT) + verify(bubbleBarViewController).animateBubbleBarLocation(bubbleBarOriginalLocation) + assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(container.childCount).isEqualTo(0) + } + + @Test + fun droppedAtTheLeftDropZone_noBubblesOnTheRight_appBubbleCreationRequested() { + val bubbleBarOriginalLocation = BubbleBarLocation.RIGHT + prepareBubbleBarViewController( + hasBubbles = false, + bubbleBarLocation = bubbleBarOriginalLocation, + ) + val packageName = "test.package" + val itemIntent = + Intent().apply { + component = ComponentName(packageName, "TestClass") + `package` = packageName + } + val appInfo = AppInfo().apply { intent = itemIntent } + dragObject.dragInfo = appInfo + + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + bubbleBarLeftDropTarget.onDrop(dragObject, DragOptions()) + assertThat(dragToBubbleController.isItemDropHandled).isTrue() + bubbleBarLeftDropTarget.onDragExit(dragObject) + } + + verify(systemUiProxy).showAppBubble(itemIntent, appInfo.user, BubbleBarLocation.LEFT) + } + + @Test + fun dragExitRightZone_noBubbles_listenerNotNotified() { + // Scenario: No bubbles. Drag enters RIGHT, then exits to no particular zone. + // This is distinct as it starts on the default side. + prepareBubbleBarViewController( + hasBubbles = false, + bubbleBarLocation = BubbleBarLocation.RIGHT, + ) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) // Location is the same + } + verify(bubbleBarLocationListener, never()) + .onBubbleBarLocationAnimated(BubbleBarLocation.RIGHT) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXY(0, 0) // Move out of all zones + bubbleBarRightDropTarget.onDragExit(dragObject) + } + + // Exiting the RIGHT zone (which is the default) should not re-notify of RIGHT + verify(bubbleBarLocationListener, never()) + .onBubbleBarLocationAnimated(BubbleBarLocation.RIGHT) + } + + @Test + fun onDragEnd_noBubbles_wasDraggingLeft_listenerNotifiedWithDefaultRightLocationAnimated() { + val startingLocation = BubbleBarLocation.RIGHT + // Scenario: No bubbles. Drag was over LEFT zone. Drag ends. + prepareBubbleBarViewController(hasBubbles = false, bubbleBarLocation = startingLocation) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + } + // Notifies onBubbleBarLocationAnimated(LEFT) + verify(bubbleBarLocationListener).onBubbleBarLocationAnimated(BubbleBarLocation.LEFT) + clearInvocations(bubbleBarLocationListener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragToBubbleController.onDragEnd() + } + assertThat(dragToBubbleController.isItemDropHandled).isFalse() + + // After drag ends (and no bubbles), the listener should be notified of the default location + verify(bubbleBarLocationListener).onBubbleBarLocationAnimated(startingLocation) + } + + @Test + fun onDragEnd_noBubbles_wasDraggingRight_listenerNotifiedWithDefaultRightLocationAnimated() { + // Scenario: No bubbles. Drag was over RIGHT zone (default side). Drag ends. + prepareBubbleBarViewController(hasBubbles = false) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + } + verify(bubbleBarLocationListener, never()) + .onBubbleBarLocationAnimated(BubbleBarLocation.RIGHT) + clearInvocations(bubbleBarLocationListener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXY(0, 0) + bubbleBarLeftDropTarget.onDragExit(dragObject) + dragToBubbleController.onDragEnd() + } + + // After drag ends (and no bubbles), listener should not be notified of the default + // location. + verify(bubbleBarLocationListener, never()) + .onBubbleBarLocationAnimated(BubbleBarLocation.RIGHT) + } + + @Test + fun dragEnterLeftZone_bubblesOnLeft_listenerNotNotified() { + // Scenario: Bubbles on LEFT. Drag enters LEFT zone. + prepareBubbleBarViewController( + hasBubbles = true, + bubbleBarLocation = BubbleBarLocation.LEFT, + ) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + } + + // Bubbles are already on the LEFT, and drag enters LEFT. + // No new animation to LEFT should be triggered by the zone entry itself. + verify(bubbleBarLocationListener, never()) + .onBubbleBarLocationAnimated(BubbleBarLocation.LEFT) + } + + @Test + fun onDragEnd_bubblesOnLeft_defaultIsLeft_wasDraggingRight_listenerNotifiedLeftAnimated() { + // Scenario: Bubbles on LEFT. Drag was over RIGHT zone. Drag ends. + prepareBubbleBarViewController( + hasBubbles = true, + bubbleBarLocation = BubbleBarLocation.LEFT, + ) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) // Notifies Animated(RIGHT) + } + verify(bubbleBarLocationListener).onBubbleBarLocationAnimated(BubbleBarLocation.RIGHT) + clearInvocations(bubbleBarLocationListener) // Clear the Animated(RIGHT) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXY(0, 0) + bubbleBarLeftDropTarget.onDragExit(dragObject) + dragToBubbleController.onDragEnd() + } + + // Bubble bar's final animated location should be LEFT. + verify(bubbleBarLocationListener).onBubbleBarLocationAnimated(BubbleBarLocation.LEFT) + } + + @Test + fun dragEnterLeftThenExitToNoZoneThenEnterRight_noBubbles_listenerSequenceCorrectAnimated() { + // Scenario: No bubbles. Complex drag path: Left -> None -> Right + prepareBubbleBarViewController(hasBubbles = false) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + clearInvocations(bubbleBarLocationListener) + + val inOrder = inOrder(bubbleBarLocationListener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // 1. Enter Left + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + + // 2. Exit Left to no zone + dragObject.updateXY(0, 0) + bubbleBarLeftDropTarget.onDragExit(dragObject) + + // 3. Enter Right + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) + } + + inOrder + .verify(bubbleBarLocationListener) + .onBubbleBarLocationAnimated(BubbleBarLocation.LEFT) + // Revert to default, following enter of the same zone should not trigger updated + inOrder + .verify(bubbleBarLocationListener) + .onBubbleBarLocationAnimated(BubbleBarLocation.RIGHT) + } + + private fun prepareBubbleBarViewController( + hasBubbles: Boolean = false, + bubbleBarLocation: BubbleBarLocation = BubbleBarLocation.RIGHT, + ) { + bubbleBarViewController.stub { + on { hasBubbles() } doReturn hasBubbles + on { getBubbleBarLocation() } doReturn bubbleBarLocation + } + } + + private fun DragObject.updateXYToCenterOf(rect: Rect) { + updateXY(rect.centerX(), rect.centerY()) + } + + private fun DragObject.updateXY(x: Int, y: Int) { + this.x = x + this.y = y + } + + private fun createTestDragZoneFactory(): DragZoneFactory { + val deviceConfig = + DeviceConfig( + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + insets = Insets.NONE, + ) + return DragZoneFactory( + context, + deviceConfig, + { SplitScreenMode.NONE }, + { false }, + bubbleBarPropertiesProvider, + ) + } + + private class FakeBubbleBarPropertiesProvider : BubbleBarPropertiesProvider { + + override fun getHeight(): Int = BUBBLE_BAR_HEIGHT + + override fun getWidth(): Int = BUBBLE_BAR_WIDTH + + override fun getBottomPadding(): Int = BUBBLE_BAR_BOTTOM_PADDING + } + + companion object { + const val BUBBLE_BAR_WIDTH = 100 + const val BUBBLE_BAR_HEIGHT = 110 + const val BUBBLE_BAR_BOTTOM_PADDING = 70 + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + const val DROP_VIEWS_COUNT = 1 + const val DROP_VIEWS_COUNT_NO_BUBBLE_BAR = 2 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt new file mode 100644 index 0000000000..da362bdfb3 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt @@ -0,0 +1,119 @@ +/* + * 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.launcher3.taskbar.bubbles.animation + +import androidx.core.animation.AnimatorTestRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleAnimatorTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private lateinit var bubbleAnimator: BubbleAnimator + + @Test + fun animateNewBubble_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false, + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateNewBubble(selectedBubbleIndex = 2, listener = listener) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + @Test + fun animateRemovedBubble_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false, + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateRemovedBubble( + bubbleIndex = 2, + selectedBubbleIndex = 3, + removingLastBubble = false, + removingLastRemainingBubble = false, + listener, + ) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + @Test + fun animateNewAndRemoveOld_isRunning() { + bubbleAnimator = + BubbleAnimator( + iconSize = 40f, + expandedBarIconSpacing = 10f, + bubbleCount = 5, + onLeft = false, + ) + val listener = TestBubbleAnimatorListener() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleAnimator.animateNewAndRemoveOld( + selectedBubbleIndex = 3, + newlySelectedBubbleIndex = 2, + removedBubbleIndex = 1, + addedBubbleIndex = 3, + listener, + ) + } + + assertThat(bubbleAnimator.isRunning).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(bubbleAnimator.isRunning).isFalse() + } + + private class TestBubbleAnimatorListener : BubbleAnimator.Listener { + + override fun onAnimationUpdate(animatedFraction: Float) {} + + override fun onAnimationCancel() {} + + override fun onAnimationEnd() {} + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt deleted file mode 100644 index cc579abc9e..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt +++ /dev/null @@ -1,454 +0,0 @@ -/* - * 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.launcher3.taskbar.bubbles.animation - -import android.content.Context -import android.graphics.Color -import android.graphics.Path -import android.graphics.drawable.ColorDrawable -import android.view.LayoutInflater -import android.view.View -import android.view.View.VISIBLE -import android.widget.FrameLayout -import androidx.core.graphics.drawable.toBitmap -import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import androidx.test.platform.app.InstrumentationRegistry -import com.android.launcher3.R -import com.android.launcher3.taskbar.bubbles.BubbleBarBubble -import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow -import com.android.launcher3.taskbar.bubbles.BubbleBarView -import com.android.launcher3.taskbar.bubbles.BubbleStashController -import com.android.launcher3.taskbar.bubbles.BubbleView -import com.android.wm.shell.common.bubbles.BubbleInfo -import com.android.wm.shell.shared.animation.PhysicsAnimator -import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@SmallTest -@RunWith(AndroidJUnit4::class) -class BubbleBarViewAnimatorTest { - - private val context = ApplicationProvider.getApplicationContext() - private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler - private lateinit var overflowView: BubbleView - private lateinit var bubbleView: BubbleView - private lateinit var bubble: BubbleBarBubble - private lateinit var bubbleBarView: BubbleBarView - private lateinit var bubbleStashController: BubbleStashController - - @Before - fun setUp() { - animatorScheduler = TestBubbleBarViewAnimatorScheduler() - PhysicsAnimatorTestUtils.prepareForTest() - } - - @Test - fun animateBubbleInForStashed() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - assertThat(handle.alpha).isEqualTo(0) - assertThat(handle.translationY) - .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) - assertThat(bubbleBarView.scaleX).isEqualTo(1) - assertThat(bubbleBarView.scaleY).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - - // execute the hide bubble animation - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - assertThat(handle.alpha).isEqualTo(1) - assertThat(handle.translationY).isEqualTo(0) - assertThat(bubbleBarView.alpha).isEqualTo(0) - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - verify(bubbleStashController).stashBubbleBarImmediate() - } - - @Test - fun animateBubbleInForStashed_tapAnimatingBubble() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - assertThat(handle.alpha).isEqualTo(0) - assertThat(handle.translationY) - .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) - assertThat(bubbleBarView.scaleX).isEqualTo(1) - assertThat(bubbleBarView.scaleY).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - - verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion() - - // verify the hide bubble animation is pending - assertThat(animatorScheduler.delayedBlock).isNotNull() - - animator.onBubbleBarTouchedWhileAnimating() - - assertThat(animatorScheduler.delayedBlock).isNull() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - } - - @Test - fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // wait for the animation to start - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } - - handleAnimator.assertIsRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - // verify the hide bubble animation is pending - assertThat(animatorScheduler.delayedBlock).isNotNull() - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.onStashStateChangingWhileAnimating() - } - - // verify that the hide animation was canceled - assertThat(animatorScheduler.delayedBlock).isNull() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any()) - - // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait - // again - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - handleAnimator.assertIsNotRunning() - } - - @Test - fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // let the animation start and wait for it to complete - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - // execute the hide bubble animation - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - // wait for the hide animation to start - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - handleAnimator.assertIsRunning() - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.onStashStateChangingWhileAnimating() - } - - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any()) - - // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait - // again - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - handleAnimator.assertIsNotRunning() - } - - @Test - fun animateBubbleInForStashed_showAnimationCanceled() { - setUpBubbleBar() - setUpBubbleStashController() - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateBubbleInForStashed(bubble) - } - - // wait for the animation to start - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } - - handleAnimator.assertIsRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(animatorScheduler.delayedBlock).isNotNull() - - handleAnimator.cancel() - handleAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(animatorScheduler.delayedBlock).isNull() - } - - @Test - fun animateToInitialState_inApp() { - setUpBubbleBar() - setUpBubbleStashController() - whenever(bubbleStashController.bubbleBarTranslationY) - .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateToInitialState(bubble, isInApp = true, isExpanding = false) - } - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(bubbleBarView.alpha).isEqualTo(0) - assertThat(handle.translationY).isEqualTo(0) - assertThat(handle.alpha).isEqualTo(1) - - verify(bubbleStashController).stashBubbleBarImmediate() - } - - @Test - fun animateToInitialState_inApp_autoExpanding() { - setUpBubbleBar() - setUpBubbleStashController() - whenever(bubbleStashController.bubbleBarTranslationY) - .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) - - val handle = View(context) - val handleAnimator = PhysicsAnimator.getInstance(handle) - whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator) - - val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) - } - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) - - verify(bubbleStashController).showBubbleBarImmediate() - } - - @Test - fun animateToInitialState_inHome() { - setUpBubbleBar() - setUpBubbleStashController() - whenever(bubbleStashController.bubbleBarTranslationY) - .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT) - - val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) - - val animator = - BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) - - InstrumentationRegistry.getInstrumentation().runOnMainSync { - animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) - } - - InstrumentationRegistry.getInstrumentation().runOnMainSync {} - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) - - barAnimator.assertIsNotRunning() - assertThat(bubbleBarView.isAnimatingNewBubble).isTrue() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) - - assertThat(animatorScheduler.delayedBlock).isNotNull() - InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) - - assertThat(bubbleBarView.isAnimatingNewBubble).isFalse() - assertThat(bubbleBarView.alpha).isEqualTo(1) - assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) - - verify(bubbleStashController).showBubbleBarImmediate() - } - - private fun setUpBubbleBar() { - bubbleBarView = BubbleBarView(context) - InstrumentationRegistry.getInstrumentation().runOnMainSync { - bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0) - val inflater = LayoutInflater.from(context) - - val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) - overflowView = - inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView - overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) - bubbleBarView.addView(overflowView) - - val bubbleInfo = - BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false) - bubbleView = - inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView - bubble = - BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "") - bubbleView.setBubble(bubble) - bubbleBarView.addView(bubbleView) - } - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - } - - private fun setUpBubbleStashController() { - bubbleStashController = mock() - whenever(bubbleStashController.isStashed).thenReturn(true) - whenever(bubbleStashController.diffBetweenHandleAndBarCenters) - .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS) - whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation) - .thenReturn(HANDLE_TRANSLATION) - whenever(bubbleStashController.bubbleBarTranslationYForTaskbar) - .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) - } - - private fun PhysicsAnimator.assertIsRunning() { - InstrumentationRegistry.getInstrumentation().runOnMainSync { - assertThat(isRunning()).isTrue() - } - } - - private fun PhysicsAnimator.assertIsNotRunning() { - InstrumentationRegistry.getInstrumentation().runOnMainSync { - assertThat(isRunning()).isFalse() - } - } - - private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler { - - var delayedBlock: Runnable? = null - private set - - override fun post(block: Runnable) { - block.run() - } - - override fun postDelayed(delayMillis: Long, block: Runnable) { - check(delayedBlock == null) { "there is already a pending block waiting to run" } - delayedBlock = block - } - - override fun cancel(block: Runnable) { - check(delayedBlock == block) { "the pending block does not match the canceled block" } - delayedBlock = null - } - } -} - -private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f -private const val HANDLE_TRANSLATION = -30f -private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f -private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt new file mode 100644 index 0000000000..91fe6a6f02 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt @@ -0,0 +1,256 @@ +/* + * 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.launcher3.taskbar.bubbles.flyout + +import android.content.Context +import android.graphics.Color +import android.graphics.PointF +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for [BubbleBarFlyoutController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarFlyoutControllerTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private lateinit var flyoutController: BubbleBarFlyoutController + private lateinit var flyoutContainer: FrameLayout + private lateinit var flyoutCallbacks: FakeFlyoutCallbacks + private val context = ApplicationProvider.getApplicationContext() + private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message") + private var onLeft = true + private var flyoutTy = 50f + + private val showAnimationDuration = 400L + private val hideAnimationDuration = 350L + + @Before + fun setUp() { + flyoutContainer = FrameLayout(context) + val positioner = + object : BubbleBarFlyoutPositioner { + override val isOnLeft + get() = onLeft + + override val targetTy + get() = flyoutTy + + override val distanceToCollapsedPosition = PointF(100f, 200f) + override val collapsedSize = 30f + override val collapsedColor = Color.BLUE + override val collapsedElevation = 1f + override val distanceToRevealTriangle = 50f + } + flyoutCallbacks = FakeFlyoutCallbacks() + val flyoutScheduler = FlyoutScheduler { block -> block.invoke() } + flyoutController = + BubbleBarFlyoutController(flyoutContainer, positioner, flyoutCallbacks, flyoutScheduler) + } + + @Test + fun flyoutPosition_left() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT) + assertThat(flyout.translationY).isEqualTo(50f) + } + } + + @Test + fun flyoutPosition_right() { + onLeft = false + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val lp = flyout.layoutParams as FrameLayout.LayoutParams + assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT) + assertThat(flyout.translationY).isEqualTo(50f) + } + } + + @Test + fun flyoutMessage() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyout = flyoutContainer.getChildAt(0) + val sender = flyout.findViewById(R.id.bubble_flyout_title) + assertThat(sender.text).isEqualTo("sender name") + val message = flyout.findViewById(R.id.bubble_flyout_text) + assertThat(message.text).isEqualTo("message") + } + } + + @Test + fun hideFlyout_removedFromContainer() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutController.hasFlyout()).isTrue() + assertThat(flyoutContainer.childCount).isEqualTo(1) + flyoutController.collapseFlyout {} + animatorTestRule.advanceTimeBy(hideAnimationDuration) + } + assertThat(flyoutContainer.childCount).isEqualTo(0) + assertThat(flyoutController.hasFlyout()).isFalse() + } + + @Test + fun cancelFlyout_fadesOutFlyout() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyoutView = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyoutView.alpha).isEqualTo(1f) + flyoutController.cancelFlyout {} + animatorTestRule.advanceTimeBy(hideAnimationDuration) + assertThat(flyoutView.alpha).isEqualTo(0f) + } + } + + @Test + fun clickFlyout_notifiesCallback() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutContainer.childCount).isEqualTo(1) + val flyoutView = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyoutView.alpha).isEqualTo(1f) + animatorTestRule.advanceTimeBy(showAnimationDuration) + flyoutView.performClick() + } + assertThat(flyoutCallbacks.flyoutClicked).isTrue() + } + + @Test + fun updateFlyoutWhileExpanding() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + assertThat(flyoutController.hasFlyout()).isTrue() + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + // advance the animation about halfway + animatorTestRule.advanceTimeBy(100) + } + assertThat(flyoutController.hasFlyout()).isTrue() + + val newFlyoutMessage = flyoutMessage.copy(message = "new message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + // set negative translation to verify that the top boundary extends as a result of + // updating while expanding + flyout.translationY = -50f + flyoutController.updateFlyoutWhileExpanding(newFlyoutMessage) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("new message") + } + } + + @Test + fun updateFlyoutFullyExpanded() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + animatorTestRule.advanceTimeBy(showAnimationDuration) + } + assertThat(flyoutController.hasFlyout()).isTrue() + + val newFlyoutMessage = flyoutMessage.copy(message = "new message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + // set negative translation to verify that the top boundary extends as a result of + // updating while fully expanded + flyout.translationY = -50f + flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {} + + // advance the timer so that the fade out animation plays + animatorTestRule.advanceTimeBy(hideAnimationDuration) + assertThat(flyout.alpha).isEqualTo(0) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("new message") + + // advance the timer so that the fade in animation plays + animatorTestRule.advanceTimeBy(showAnimationDuration) + assertThat(flyout.alpha).isEqualTo(1) + } + } + + @Test + fun updateFlyoutWhileCollapsing() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + setupAndShowFlyout() + animatorTestRule.advanceTimeBy(showAnimationDuration) + } + assertThat(flyoutController.hasFlyout()).isTrue() + + val newFlyoutMessage = flyoutMessage.copy(message = "new message") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + var flyoutCollapsed = false + flyoutController.collapseFlyout { flyoutCollapsed = true } + // advance the fake timer so that the collapse animation runs for 125ms + animatorTestRule.advanceTimeBy(125) + + // update the flyout in the middle of collapsing, which should start expanding it. + var flyoutReversed = false + flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true } + + // the collapse and expand animations use an emphasized interpolator, so the reverse + // path does not take the same time. advance the timer the by full duration of the show + // animation to ensure it completes + animatorTestRule.advanceTimeBy(showAnimationDuration) + val flyout = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + assertThat(flyout.alpha).isEqualTo(1) + assertThat(flyout.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("new message") + // verify that we never called the end action on the collapse animation + assertThat(flyoutCollapsed).isFalse() + // verify that we called the end action on the reverse animation + assertThat(flyoutReversed).isTrue() + } + assertThat(flyoutController.hasFlyout()).isTrue() + } + + private fun setupAndShowFlyout() { + flyoutController.setUpAndShowFlyout(flyoutMessage, {}, {}) + } + + class FakeFlyoutCallbacks : FlyoutCallbacks { + + var flyoutClicked = false + + override fun flyoutClicked() { + flyoutClicked = true + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt new file mode 100644 index 0000000000..0421a13aec --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt @@ -0,0 +1,436 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import android.animation.AnimatorTestRule +import android.content.Context +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.launcher3.util.MultiValueAlpha +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Unit tests for [PersistentBubbleStashController]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class PersistentBubbleStashControllerTest { + + companion object { + const val BUBBLE_BAR_HEIGHT = 100f + const val HOTSEAT_VERTICAL_CENTER = 95 + const val HOTSEAT_TRANSLATION_Y = -45f + const val TASK_BAR_TRANSLATION_Y = -5f + } + + @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + + @get:Rule val rule: MockitoRule = MockitoJUnit.rule() + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleBarView: BubbleBarView + + @Mock lateinit var bubbleBarViewController: BubbleBarViewController + + @Mock lateinit var taskbarInsetsController: TaskbarInsetsController + + private lateinit var persistentTaskBarStashController: PersistentBubbleStashController + private lateinit var translationY: AnimatedFloat + private lateinit var scale: AnimatedFloat + private lateinit var alpha: MultiValueAlpha + + @Before + fun setUp() { + persistentTaskBarStashController = + PersistentBubbleStashController(DefaultDimensionsProvider()) + setUpBubbleBarView() + setUpBubbleBarController() + persistentTaskBarStashController.bubbleBarVerticalCenterForHome = HOTSEAT_VERTICAL_CENTER + persistentTaskBarStashController.init( + taskbarInsetsController, + bubbleBarViewController, + null, + ImmediateAction(), + ) + } + + @Test + fun updateLauncherState_noBubbles_controllerNotified() { + // Given bubble bar has no bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + + // When switch to home screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + } + + // Then bubble bar view controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false) + } + + @Test + fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() { + // Given bubble bar is on home and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + } + + // Then translation Y is animating and the bubble bar controller is notified + assertThat(translationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + // Check translation Y is correct and the insets controller is notified + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() { + // Given bubble bar has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch to home screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + } + + // Then translation Y is animating and the bubble bar controller is notified + assertThat(translationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + + // Check translation Y is correct and the insets controller is notified + assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y) + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToFalse_controllersNotified() { + // Given bubble bar is on overview + persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW + clearInvocations(bubbleBarViewController) + + // When switch out of the overview screen + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + // Then bubble bar controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToTrue_controllersNotified() { + // When switch to the overview screen + persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW + + // Then bubble bar controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + } + + @Test + fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() { + // Given screen is locked and bubble bar has bubbles + persistentTaskBarStashController.isSysuiLocked = true + persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch to the overview screen + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.isSysuiLocked = false + } + + // Then + assertThat(translationY.isAnimating).isTrue() + assertThat(scale.isAnimating).isTrue() + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + + // Then bubble bar is fully visible at the correct location + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // Insets controller is notified + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun showBubbleBarImmediateToY() { + // Given bubble bar is fully transparent and scaled to 0 at 0 y position + val targetY = 341f + bubbleBarView.alpha = 0f + bubbleBarView.scaleX = 0f + bubbleBarView.scaleY = 0f + bubbleBarView.translationY = 0f + + // When + persistentTaskBarStashController.showBubbleBarImmediate(targetY) + + // Then all property values are updated + assertThat(bubbleBarView.translationY).isEqualTo(targetY) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + } + + @Test + fun isTransientTaskbar_false() { + assertThat(persistentTaskBarStashController.isTransientTaskBar).isFalse() + } + + @Test + fun hasHandleView_false() { + assertThat(persistentTaskBarStashController.hasHandleView).isFalse() + } + + @Test + fun isStashed_false() { + assertThat(persistentTaskBarStashController.isStashed).isFalse() + } + + @Test + fun bubbleBarTranslationYForTaskbar() { + // Give bubble bar is on home + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + + // Then bubbleBarTranslationY would be HOTSEAT_TRANSLATION_Y + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(HOTSEAT_TRANSLATION_Y) + + // Give bubble bar is not on home + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + // Then bubbleBarTranslationY would be TASK_BAR_TRANSLATION_Y + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun inAppDisplayOverrideProgress_onHome_updatesTranslationFromHomeToInApp() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(HOTSEAT_TRANSLATION_Y) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + + val middleBetweenHotseatAndTaskbar = (HOTSEAT_TRANSLATION_Y + TASK_BAR_TRANSLATION_Y) / 2f + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isWithin(0.1f) + .of(middleBetweenHotseatAndTaskbar) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesOne() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + // Reset invocations to track only changes from in-app display override + clearInvocations(taskbarInsetsController) + + // Insets are not updated for values between 0 and 1 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + + // Update insets when progress reaches 1 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesZero() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + // Reset invocations to track only changes from in-app display override + clearInvocations(taskbarInsetsController) + + // Insets are not updated for values between 0 and 1 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + + // Update insets when progress reaches 0 + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun inAppDisplayOverrideProgress_onHome_cancelExistingAnimation() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME + + bubbleBarViewController.bubbleBarTranslationY.animateToValue(100f) + advanceTimeBy(10) + assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isTrue() + + getInstrumentation().runOnMainSync { + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + } + assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isFalse() + } + + @Test + fun inAppDisplayProgressUpdate_inApp_noTranslationUpdate() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + + assertThat(persistentTaskBarStashController.bubbleBarTranslationY) + .isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun inAppDisplayOverrideProgress_inApp_noInsetsUpdate() { + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP + + // Reset invocations to track only changes from in-app display override + clearInvocations(taskbarInsetsController) + + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f + persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f + persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f + + // Never triggers an update to insets + verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun showBubbleBar_expand_bubbleBarGesture() { + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + persistentTaskBarStashController.showBubbleBar( + expandBubbles = true, + bubbleBarGesture = true, + ) + + verify(bubbleBarViewController).animateExpanded(true, true) + } + + @Test + fun showBubbleBar_expand_notBubbleBarGesture() { + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + persistentTaskBarStashController.showBubbleBar( + expandBubbles = true, + bubbleBarGesture = false, + ) + + verify(bubbleBarViewController).animateExpanded(true, false) + } + + @Test + fun showBubbleBar_notExpanding_bubbleBarGesture() { + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + persistentTaskBarStashController.showBubbleBar( + expandBubbles = false, + bubbleBarGesture = true, + ) + + verify(bubbleBarViewController, never()).animateExpanded(any(), any()) + } + + private fun advanceTimeBy(advanceMs: Long) { + // Advance animator for on-device tests + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) } + } + + private fun setUpBubbleBarView() { + getInstrumentation().runOnMainSync { + bubbleBarView = BubbleBarView(context) + bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0) + bubbleBarView.setController( + object : BubbleBarView.Controller { + override fun getBubbleBarTranslationY(): Float = 0f + + override fun onBubbleBarTouched() {} + + override fun expandBubbleBar() {} + + override fun dismissBubbleBar() {} + + override fun updateBubbleBarLocation( + location: BubbleBarLocation?, + source: Int, + ) {} + + override fun setIsDragging(dragging: Boolean) {} + + override fun onBubbleBarExpandedStateChanged(expanded: Boolean) {} + } + ) + } + } + + private fun setUpBubbleBarController() { + translationY = AnimatedFloat(Runnable { bubbleBarView.translationY = translationY.value }) + scale = + AnimatedFloat( + Runnable { + val scale: Float = scale.value + bubbleBarView.scaleX = scale + bubbleBarView.scaleY = scale + } + ) + alpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */) + + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(translationY) + whenever(bubbleBarViewController.bubbleBarScaleY).thenReturn(scale) + whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(alpha) + whenever(bubbleBarViewController.bubbleBarCollapsedHeight).thenReturn(BUBBLE_BAR_HEIGHT) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt new file mode 100644 index 0000000000..96c2f4554f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt @@ -0,0 +1,35 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +class ImmediateAction : BubbleStashController.ControllersAfterInitAction { + override fun runAfterInit(action: Runnable) = action.run() +} + +class DefaultDimensionsProvider( + private val taskBarBottomSpace: Int = TASKBAR_BOTTOM_SPACE, + private val taskBarHeight: Int = TASKBAR_HEIGHT, +) : BubbleStashController.TaskbarHotseatDimensionsProvider { + override fun getTaskbarBottomSpace(): Int = taskBarBottomSpace + + override fun getTaskbarHeight(): Int = taskBarHeight + + companion object { + const val TASKBAR_BOTTOM_SPACE = 0 + const val TASKBAR_HEIGHT = 110 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt new file mode 100644 index 0000000000..3b6412d787 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt @@ -0,0 +1,643 @@ +/* + * 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.launcher3.taskbar.bubbles.stashing + +import android.animation.AnimatorSet +import android.animation.AnimatorTestRule +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.taskbar.StashedHandleView +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.TaskbarStashController +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.launcher3.util.MultiValueAlpha +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Unit tests for [TransientBubbleStashController]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class TransientBubbleStashControllerTest { + + companion object { + const val TASKBAR_BOTTOM_SPACE = 5 + const val HOTSEAT_VERTICAL_CENTER = 95 + const val BUBBLE_BAR_WIDTH = 200 + const val BUBBLE_BAR_HEIGHT = 100 + const val HOTSEAT_TRANSLATION_Y = -45f + const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE.toFloat() + const val HANDLE_VIEW_WIDTH = 150 + const val HANDLE_VIEW_HEIGHT = 4 + const val BUBBLE_BAR_STASHED_TRANSLATION_Y = -4.5f + } + + @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + + @get:Rule val rule: MockitoRule = MockitoJUnit.rule() + + @Mock lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController + + @Mock lateinit var bubbleBarViewController: BubbleBarViewController + + @Mock lateinit var taskbarInsetsController: TaskbarInsetsController + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleBarView: BubbleBarView + private lateinit var stashedHandleView: StashedHandleView + private lateinit var bubbleView: BubbleView + private lateinit var barTranslationY: AnimatedFloat + private lateinit var barScaleX: AnimatedFloat + private lateinit var barScaleY: AnimatedFloat + private lateinit var barAlpha: MultiValueAlpha + private lateinit var bubbleOffsetY: AnimatedFloat + private lateinit var bubbleAlpha: AnimatedFloat + private lateinit var backgroundAlpha: AnimatedFloat + private lateinit var stashedHandleAlpha: MultiValueAlpha + private lateinit var stashedHandleScale: AnimatedFloat + private lateinit var stashedHandleTranslationY: AnimatedFloat + private lateinit var stashPhysicsAnimator: PhysicsAnimator + + private lateinit var mTransientBubbleStashController: TransientBubbleStashController + + @Before + fun setUp() { + val taskbarHotseatDimensionsProvider = + DefaultDimensionsProvider(taskBarBottomSpace = TASKBAR_BOTTOM_SPACE) + mTransientBubbleStashController = + TransientBubbleStashController(taskbarHotseatDimensionsProvider, context) + setUpBubbleBarView() + setUpBubbleBarController() + setUpStashedHandleView() + setUpBubbleStashedHandleViewController() + PhysicsAnimatorTestUtils.prepareForTest() + mTransientBubbleStashController.bubbleBarVerticalCenterForHome = HOTSEAT_VERTICAL_CENTER + mTransientBubbleStashController.init( + taskbarInsetsController, + bubbleBarViewController, + bubbleStashedHandleViewController, + ImmediateAction(), + ) + } + + @Test + fun updateLauncherState_noBubbles_controllerNotified() { + // Given bubble bar has no bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + + // When switch to home screen + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME + } + + // Then bubble bar view controller is notified + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false) + } + + @Test + fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() { + // Given bubble bar is on home and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME + } + + // Then BubbleBarView is animating, BubbleBarViewController controller is notified + assertThat(barTranslationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + // Then translation Y is correct and the insets controller is notified + assertThat(barTranslationY.isAnimating).isFalse() + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + assertThat(bubbleBarView.translationY).isEqualTo(HOTSEAT_TRANSLATION_Y) + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToTrue_barPositionYUpdated_controllersNotified() { + // Given bubble bar is on overview and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + // When switch out of the home screen + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW + } + + // Then BubbleBarView is animating, BubbleBarViewController controller is notified + assertThat(barTranslationY.isAnimating).isTrue() + verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true) + + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + // Then translation Y is correct and the insets controller is notified + assertThat(barTranslationY.isAnimating).isFalse() + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + } + + @Test + fun setBubblesShowingOnOverviewUpdatedToTrue_unstashes() { + // Given bubble bar is stashed with bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + assertThat(mTransientBubbleStashController.isStashed).isTrue() + + // Move to overview + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW + } + // No longer stashed in overview + assertThat(mTransientBubbleStashController.isStashed).isFalse() + } + + @Test + fun updateStashedAndExpandedState_stashAndCollapse_bubbleBarHidden_stashedHandleShown() { + // Given bubble bar has bubbles and not stashed + mTransientBubbleStashController.isStashed = false + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + val bubbleInitialTranslation = bubbleView.translationY + + // When stash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + + // Wait until animations ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // Then check BubbleBarController is notified + verify(bubbleBarViewController).onStashStateChanging() + // Bubble bar is stashed + assertThat(mTransientBubbleStashController.isStashed).isTrue() + assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(0f) + assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX()) + assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY()) + assertThat(bubbleBarView.background.alpha).isEqualTo(255) + // Handle view is visible + assertThat(stashedHandleView.translationY).isEqualTo(0) + assertThat(stashedHandleView.alpha).isEqualTo(1) + // Bubble view is reset + assertThat(bubbleView.translationY).isEqualTo(bubbleInitialTranslation) + assertThat(bubbleView.alpha).isEqualTo(1f) + } + + @Test + fun updateStashedAndExpandedState_unstash_bubbleBarShown_stashedHandleHidden() { + // Given bubble bar has bubbles and is stashed + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + val bubbleInitialTranslation = bubbleView.translationY + + // When unstash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + ) + } + + // Wait until animations ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // Then check BubbleBarController is notified + verify(bubbleBarViewController).onStashStateChanging() + // Bubble bar is unstashed + assertThat(mTransientBubbleStashController.isStashed).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + assertThat(bubbleBarView.background.alpha).isEqualTo(255) + // Handle view is hidden + assertThat(stashedHandleView.translationY).isEqualTo(0) + assertThat(stashedHandleView.alpha).isEqualTo(0) + // Bubble view is reset + assertThat(bubbleView.translationY).isEqualTo(bubbleInitialTranslation) + assertThat(bubbleView.alpha).isEqualTo(1f) + } + + @Test + fun updateStashedAndExpandedState_stash_animatesAlphaForBubblesAndBackgroundSeparately() { + // Given bubble bar has bubbles and is unstashed + mTransientBubbleStashController.isStashed = false + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When stash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + + // Stop after alpha starts + advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10) + + // Bubble bar alpha is set to 1 + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // We animate alpha for background and children separately + assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f)) + assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255)) + assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt()) + } + + @Test + fun updateStashedAndExpandedState_unstash_animatesAlphaForBubblesAndBackgroundSeparately() { + // Given bubble bar has bubbles and is stashed + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When unstash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + ) + } + + // Stop after alpha starts + advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10) + + // Bubble bar alpha is set to 1 + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // We animate alpha for background and children separately + assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f)) + assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255)) + assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt()) + } + + @Test + fun updateStashedAndExpandedState_stash_updateBarVisibilityAfterAnimation() { + // Given bubble bar has bubbles and is unstashed + mTransientBubbleStashController.isStashed = false + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When stash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = true, + expand = false, + ) + } + + // Hides bubble bar only after animation completes + verify(bubbleBarViewController, never()).setHiddenForStashed(true) + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + verify(bubbleBarViewController).setHiddenForStashed(true) + } + + @Test + fun updateStashedAndExpandedState_unstash_updateBarVisibilityBeforeAnimation() { + // Given bubble bar has bubbles and is stashed + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + + // When unstash + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + ) + } + + // Shows bubble bar immediately + verify(bubbleBarViewController).setHiddenForStashed(false) + } + + @Test + fun updateStashedAndExpandedState_expand_bubbleBarGesture() { + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = true, + bubbleBarGesture = true, + ) + } + + verify(bubbleBarViewController).animateExpanded(true, true) + } + + @Test + fun updateStashedAndExpandedState_expand_notBubbleBarGesture() { + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = true, + bubbleBarGesture = false, + ) + } + + verify(bubbleBarViewController).animateExpanded(true, false) + } + + @Test + fun updateStashedAndExpandedState_notExpanding_bubbleBarGesture() { + mTransientBubbleStashController.isStashed = true + whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.updateStashedAndExpandedState( + stash = false, + expand = false, + bubbleBarGesture = true, + ) + } + + verify(bubbleBarViewController, never()).animateExpanded(any(), any()) + } + + @Test + fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() { + // Given screen is locked and bubble bar has bubbles + getInstrumentation().runOnMainSync { + mTransientBubbleStashController.isSysuiLocked = true + mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + } + advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION) + + // When switch to the overview screen + getInstrumentation().runOnMainSync { mTransientBubbleStashController.isSysuiLocked = false } + + // Then + assertThat(barTranslationY.isAnimating).isTrue() + assertThat(barScaleX.isAnimating).isTrue() + // Wait until animation ends + advanceTimeBy(BubbleStashController.BAR_STASH_DURATION) + + // Then bubble bar is fully visible at the correct location + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + assertThat(bubbleBarView.translationY) + .isEqualTo(PersistentBubbleStashControllerTest.TASK_BAR_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + // Insets controller is notified + verify(taskbarInsetsController, atLeastOnce()) + .onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + } + + @Test + fun showBubbleBarImmediateToY() { + // Given bubble bar is fully transparent and scaled to 0 at 0 y position + val targetY = 341f + bubbleBarView.alpha = 0f + bubbleBarView.scaleX = 0f + bubbleBarView.scaleY = 0f + bubbleBarView.translationY = 0f + stashedHandleView.translationY = targetY + + // When + mTransientBubbleStashController.showBubbleBarImmediate(targetY) + + // Then all property values are updated + assertThat(bubbleBarView.translationY).isEqualTo(targetY) + assertThat(bubbleBarView.alpha).isEqualTo(1f) + assertThat(bubbleBarView.scaleX).isEqualTo(1f) + assertThat(bubbleBarView.scaleY).isEqualTo(1f) + // Handle is transparent + assertThat(stashedHandleView.alpha).isEqualTo(0) + // Insets controller is notified + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + // Bubble bar visibility updated + verify(bubbleBarViewController).setHiddenForStashed(false) + } + + @Test + fun stashBubbleBarImmediate() { + // When + mTransientBubbleStashController.stashBubbleBarImmediate() + + // Then all property values are updated + assertThat(bubbleBarView.translationY).isEqualTo(BUBBLE_BAR_STASHED_TRANSLATION_Y) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(bubbleBarView.scaleX).isEqualTo(mTransientBubbleStashController.getStashScaleX()) + assertThat(bubbleBarView.scaleY).isEqualTo(mTransientBubbleStashController.getStashScaleY()) + // Handle is visible at correct Y position + assertThat(stashedHandleView.alpha).isEqualTo(1) + assertThat(stashedHandleView.translationY).isEqualTo(0) + // Insets controller is notified + verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged() + // Bubble bar visibility updated + verify(bubbleBarViewController).setHiddenForStashed(true) + } + + @Test + fun getTouchableHeight_stashed_stashHeightReturned() { + // When + mTransientBubbleStashController.isStashed = true + val height = mTransientBubbleStashController.getTouchableHeight() + + // Then + assertThat(height).isEqualTo(HANDLE_VIEW_HEIGHT) + } + + @Test + fun getTouchableHeight_unstashed_barHeightReturned() { + // When BubbleBar is not stashed + mTransientBubbleStashController.isStashed = false + val height = mTransientBubbleStashController.getTouchableHeight() + + // Then bubble bar height is returned + assertThat(height).isEqualTo(BUBBLE_BAR_HEIGHT) + } + + @Test + fun getHandleViewAlpha_stashedHasBubbles_alphaPropertyReturned() { + // Given BubbleBar is stashed and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + mTransientBubbleStashController.isStashed = true + + // When handle view alpha property + val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha() + + // Then the stash handle alpha property should not be null + assertThat(alphaProperty).isNotNull() + } + + @Test + fun getHandleViewAlpha_stashedHasNoBubblesBar_alphaPropertyIsNull() { + // Given BubbleBar is stashed and has no bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(false) + mTransientBubbleStashController.isStashed = true + + // When handle view alpha property + val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha() + + // Then the stash handle alpha property should be null + assertThat(alphaProperty).isNull() + } + + @Test + fun getHandleViewAlpha_unstashedHasBubbles_alphaPropertyIsNull() { + // Given BubbleBar is not stashed and has bubbles + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + mTransientBubbleStashController.isStashed = false + + // When handle view alpha property + val alphaProperty = mTransientBubbleStashController.getHandleViewAlpha() + + // Then the stash handle alpha property should be null + assertThat(alphaProperty).isNull() + } + + private fun advanceTimeBy(advanceMs: Long) { + // Advance animator for on-device tests + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) } + } + + private fun setUpBubbleBarView() { + getInstrumentation().runOnMainSync { + bubbleBarView = BubbleBarView(context) + bubbleBarView.layoutParams = + FrameLayout.LayoutParams(BUBBLE_BAR_WIDTH, BUBBLE_BAR_HEIGHT) + bubbleBarView.setController( + object : BubbleBarView.Controller { + override fun getBubbleBarTranslationY(): Float = 0f + + override fun onBubbleBarTouched() {} + + override fun expandBubbleBar() {} + + override fun dismissBubbleBar() {} + + override fun updateBubbleBarLocation( + location: BubbleBarLocation?, + source: Int, + ) {} + + override fun setIsDragging(dragging: Boolean) {} + + override fun onBubbleBarExpandedStateChanged(expanded: Boolean) {} + } + ) + bubbleView = BubbleView(context) + bubbleBarView.addBubble(bubbleView, false) + bubbleBarView.layout(0, 0, BUBBLE_BAR_WIDTH, BUBBLE_BAR_HEIGHT) + } + } + + private fun setUpStashedHandleView() { + getInstrumentation().runOnMainSync { + stashedHandleView = StashedHandleView(context) + stashedHandleView.layoutParams = + FrameLayout.LayoutParams(HANDLE_VIEW_WIDTH, HANDLE_VIEW_HEIGHT) + } + } + + private fun setUpBubbleBarController() { + barTranslationY = + AnimatedFloat(Runnable { bubbleBarView.translationY = barTranslationY.value }) + bubbleOffsetY = AnimatedFloat { value -> bubbleBarView.setBubbleOffsetY(value) } + barScaleX = AnimatedFloat { value -> bubbleBarView.scaleX = value } + barScaleY = AnimatedFloat { value -> bubbleBarView.scaleY = value } + barAlpha = MultiValueAlpha(bubbleBarView, 1 /* num alpha channels */) + bubbleAlpha = AnimatedFloat { value -> bubbleBarView.setBubbleAlpha(value) } + backgroundAlpha = AnimatedFloat { value -> bubbleBarView.setBackgroundAlpha(value) } + + whenever(bubbleBarViewController.hasBubbles()).thenReturn(true) + whenever(bubbleBarViewController.bubbleBarTranslationY).thenReturn(barTranslationY) + whenever(bubbleBarViewController.bubbleOffsetY).thenReturn(bubbleOffsetY) + whenever(bubbleBarViewController.bubbleBarBackgroundScaleX).thenReturn(barScaleX) + whenever(bubbleBarViewController.bubbleBarBackgroundScaleY).thenReturn(barScaleY) + whenever(bubbleBarViewController.bubbleBarAlpha).thenReturn(barAlpha) + whenever(bubbleBarViewController.bubbleBarBubbleAlpha).thenReturn(bubbleAlpha) + whenever(bubbleBarViewController.bubbleBarBackgroundAlpha).thenReturn(backgroundAlpha) + whenever(bubbleBarViewController.bubbleBarCollapsedWidth) + .thenReturn(BUBBLE_BAR_WIDTH.toFloat()) + whenever(bubbleBarViewController.bubbleBarCollapsedHeight) + .thenReturn(BUBBLE_BAR_HEIGHT.toFloat()) + whenever(bubbleBarViewController.createRevealAnimatorForStashChange(any())) + .thenReturn(AnimatorSet()) + } + + private fun setUpBubbleStashedHandleViewController() { + stashedHandleTranslationY = + AnimatedFloat(Runnable { stashedHandleView.translationY = barTranslationY.value }) + stashedHandleScale = + AnimatedFloat( + Runnable { + val scale: Float = barScaleX.value + bubbleBarView.scaleX = scale + bubbleBarView.scaleY = scale + } + ) + stashedHandleAlpha = MultiValueAlpha(stashedHandleView, 1 /* num alpha channels */) + stashPhysicsAnimator = PhysicsAnimator.getInstance(stashedHandleView) + whenever(bubbleStashedHandleViewController.stashedHandleAlpha) + .thenReturn(stashedHandleAlpha) + whenever(bubbleStashedHandleViewController.physicsAnimator).thenReturn(stashPhysicsAnimator) + whenever(bubbleStashedHandleViewController.stashedWidth).thenReturn(HANDLE_VIEW_WIDTH) + whenever(bubbleStashedHandleViewController.stashedHeight).thenReturn(HANDLE_VIEW_HEIGHT) + whenever(bubbleStashedHandleViewController.setTranslationYForSwipe(any())).thenAnswer { + invocation -> + (invocation.arguments[0] as Float).also { stashedHandleView.translationY = it } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/growth/NudgeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/growth/NudgeControllerTest.kt new file mode 100644 index 0000000000..3afbca3458 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/growth/NudgeControllerTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.growth + +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.rules.TaskbarModeRule +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class NudgeControllerTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + + @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) + + @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + @InjectController lateinit var nudgeController: NudgeController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + private val wasInTestHarness = Utilities.isRunningInTestHarness() + + @Before + fun disableRunningInTestHarnessForTests() { + Utilities.disableRunningInTestHarnessForTests() + } + + @After + fun maybeEnableRunningInTestHarnessForTests() { + if (wasInTestHarness) { + Utilities.enableRunningInTestHarnessForTests() + } + } + + @Test + @TaskbarMode(TRANSIENT) + fun testShow_doesShowNudge() { + runOnMainSync { showNudge() } // Then show it + assertThat(nudgeController.isNudgeOpen).isTrue() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testHide_whenNudgeIsOpen_shouldCloseNudge() { + assertThat(nudgeController.isNudgeOpen).isFalse() + runOnMainSync { showNudge() } + assertThat(nudgeController.isNudgeOpen).isTrue() + runOnMainSync { nudgeController.hide() } + assertThat(nudgeController.isNudgeOpen).isFalse() + } + + @Test + @TaskbarMode(TRANSIENT) + fun testShow_whenTaskbarIsTransient_shouldNotShowNudge() { + assertThat(nudgeController.isNudgeOpen).isFalse() + runOnMainSync { nudgeController.init(taskbarContext.controllers) } + assertThat(nudgeController.isNudgeOpen).isFalse() + } + + private fun showNudge() { + val nudgePayload = + NudgePayload( + titleText = "Nudge title", + bodyText = "Nudge body text.", + image = Image.ResourceId(R.drawable.ic_apps), + primaryButton = + ButtonPayload( + label = "Get perk", + actions = listOf(Action.Dismiss(), Action.OpenUrl("https://www.google.com")), + ), + secondaryButton = + ButtonPayload(label = "Dismiss", actions = listOf(Action.Dismiss())), + ) + runOnMainSync { nudgeController.maybeShow(nudgePayload) } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt index c8f79463b1..579c10f712 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactoryTest.kt @@ -14,6 +14,7 @@ import androidx.test.runner.AndroidJUnit4 import com.android.launcher3.DeviceProfile import com.android.launcher3.R import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION +import com.android.launcher3.deviceprofile.DeviceProperties import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_CONTEXTUAL_BUTTONS import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_END_NAV_BUTTONS import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.ID_START_CONTEXTUAL_BUTTONS @@ -50,6 +51,8 @@ class NavButtonLayoutFactoryTest { whenever(mockNavLayout.findViewById(R.id.back)).thenReturn(mockBackButton) whenever(mockNavLayout.findViewById(R.id.home)).thenReturn(mockHomeButton) whenever(mockNavLayout.findViewById(R.id.recent_apps)).thenReturn(mockRecentsButton) + val devicePropertiesMock: DeviceProperties = mock() + whenever(mockDeviceProfile.deviceProperties).thenReturn(devicePropertiesMock) // Init top level layout whenever(mockParentButtonContainer.requireViewById(ID_END_NAV_BUTTONS)) @@ -73,7 +76,7 @@ class NavButtonLayoutFactoryTest { isInSetup = false, isThreeButtonNav = false, phoneMode = false, - surfaceRotation = surfaceRotation + surfaceRotation = surfaceRotation, ) assert(layoutter is KidsNavLayoutter) } @@ -88,7 +91,7 @@ class NavButtonLayoutFactoryTest { isInSetup = true, isThreeButtonNav = false, phoneMode = false, - surfaceRotation = surfaceRotation + surfaceRotation = surfaceRotation, ) assert(layoutter is SetupNavLayoutter) } @@ -103,7 +106,7 @@ class NavButtonLayoutFactoryTest { isInSetup = false, isThreeButtonNav = false, phoneMode = false, - surfaceRotation = surfaceRotation + surfaceRotation = surfaceRotation, ) assert(layoutter is TaskbarNavLayoutter) } @@ -117,7 +120,7 @@ class NavButtonLayoutFactoryTest { isInSetup = false, isThreeButtonNav = false, phoneMode = false, - surfaceRotation = surfaceRotation + surfaceRotation = surfaceRotation, ) } @@ -131,7 +134,7 @@ class NavButtonLayoutFactoryTest { isInSetup = false, isThreeButtonNav = true, phoneMode = true, - surfaceRotation = surfaceRotation + surfaceRotation = surfaceRotation, ) assert(layoutter is PhonePortraitNavLayoutter) } @@ -147,7 +150,7 @@ class NavButtonLayoutFactoryTest { isInSetup = false, isThreeButtonNav = true, phoneMode = true, - surfaceRotation = surfaceRotation + surfaceRotation = surfaceRotation, ) assert(layoutter is PhoneLandscapeNavLayoutter) } @@ -163,7 +166,7 @@ class NavButtonLayoutFactoryTest { isInSetup = false, isThreeButtonNav = true, phoneMode = true, - surfaceRotation = ROTATION_270 + surfaceRotation = ROTATION_270, ) assert(layoutter is PhoneSeascapeNavLayoutter) } @@ -178,16 +181,13 @@ class NavButtonLayoutFactoryTest { isInSetup = false, isThreeButtonNav = false, phoneMode = true, - surfaceRotation = surfaceRotation + surfaceRotation = surfaceRotation, ) assert(layoutter is PhoneGestureLayoutter) } private fun setDeviceProfileLandscape() { - // Use reflection to modify landscape field - val landscapeField = mockDeviceProfile.javaClass.getDeclaredField("isLandscape") - landscapeField.isAccessible = true - landscapeField.set(mockDeviceProfile, true) + whenever(mockDeviceProfile.deviceProperties.isLandscape).thenReturn(true) } private fun getLayoutter( @@ -195,7 +195,7 @@ class NavButtonLayoutFactoryTest { isInSetup: Boolean, isThreeButtonNav: Boolean, phoneMode: Boolean, - @Rotation surfaceRotation: Int + @Rotation surfaceRotation: Int, ): NavButtonLayoutFactory.NavButtonLayoutter { return NavButtonLayoutFactory.getUiLayoutter( deviceProfile = mockDeviceProfile, diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt index eebd8f9f16..1113129ba2 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt @@ -18,19 +18,19 @@ package com.android.launcher3.taskbar.overlay import android.app.ActivityManager.RunningTaskInfo import android.view.MotionEvent -import androidx.test.annotation.UiThreadTest -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY import com.android.launcher3.AbstractFloatingView.hasOpenView import com.android.launcher3.taskbar.TaskbarActivityContext -import com.android.launcher3.taskbar.TaskbarUnitTestRule -import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices -import com.android.launcher3.views.BaseDragLayer +import com.android.launcher3.util.TestUtil.getOnUiThread import com.android.systemui.shared.system.TaskStackChangeListeners import com.google.common.truth.Truth.assertThat import org.junit.Rule @@ -41,193 +41,185 @@ import org.junit.runner.RunWith @EmulatedDevices(["pixelFoldable2023"]) class TaskbarOverlayControllerTest { - @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule() + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) @InjectController lateinit var overlayController: TaskbarOverlayController private val taskbarContext: TaskbarActivityContext get() = taskbarUnitTestRule.activityContext @Test - @UiThreadTest fun testRequestWindow_twice_reusesWindow() { - val context1 = overlayController.requestWindow() - val context2 = overlayController.requestWindow() + val (context1, context2) = + getOnUiThread { + Pair(overlayController.requestWindow(), overlayController.requestWindow()) + } assertThat(context1).isSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() { - val context1 = overlayController.requestWindow() - overlayController.hideWindow() + val context1 = getOnUiThread { overlayController.requestWindow() } + runOnMainSync { overlayController.hideWindow() } - val context2 = overlayController.requestWindow() + val context2 = getOnUiThread { overlayController.requestWindow() } assertThat(context1).isNotSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_afterHidingOverlay_createsNewWindow() { - val context1 = overlayController.requestWindow() - TestOverlayView.show(context1) - overlayController.hideWindow() + val context1 = getOnUiThread { overlayController.requestWindow() } + runOnMainSync { + TestOverlayView.show(context1) + overlayController.hideWindow() + } - val context2 = overlayController.requestWindow() + val context2 = getOnUiThread { overlayController.requestWindow() } assertThat(context1).isNotSameInstanceAs(context2) } @Test - @UiThreadTest fun testRequestWindow_addsProxyView() { - TestOverlayView.show(overlayController.requestWindow()) + runOnMainSync { TestOverlayView.show(overlayController.requestWindow()) } assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() } @Test - @UiThreadTest fun testRequestWindow_closeProxyView_closesOverlay() { - val overlay = TestOverlayView.show(overlayController.requestWindow()) - AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY) + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { + AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY) + } assertThat(overlay.isOpen).isFalse() } @Test fun testRequestWindow_attachesDragLayer() { - lateinit var dragLayer: BaseDragLayer<*> - getInstrumentation().runOnMainSync { - dragLayer = overlayController.requestWindow().dragLayer - } - + val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer } // Allow drag layer to attach before checking. - getInstrumentation().runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() } + runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() } } @Test - @UiThreadTest fun testHideWindow_closesOverlay() { - val overlay = TestOverlayView.show(overlayController.requestWindow()) - overlayController.hideWindow() + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { overlayController.hideWindow() } assertThat(overlay.isOpen).isFalse() } @Test fun testHideWindow_detachesDragLayer() { - lateinit var dragLayer: BaseDragLayer<*> - getInstrumentation().runOnMainSync { - dragLayer = overlayController.requestWindow().dragLayer - } + val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer } // Wait for drag layer to be attached to window before hiding. - getInstrumentation().runOnMainSync { + runOnMainSync { overlayController.hideWindow() assertThat(dragLayer.isAttachedToWindow).isFalse() } } @Test - @UiThreadTest fun testTwoOverlays_closeOne_windowStaysOpen() { - val context = overlayController.requestWindow() - val overlay1 = TestOverlayView.show(context) - val overlay2 = TestOverlayView.show(context) + val (overlay1, overlay2) = + getOnUiThread { + val context = overlayController.requestWindow() + Pair(TestOverlayView.show(context), TestOverlayView.show(context)) + } - overlay1.close(false) + runOnMainSync { overlay1.close(false) } assertThat(overlay2.isOpen).isTrue() assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() } @Test - @UiThreadTest fun testTwoOverlays_closeAll_closesWindow() { - val context = overlayController.requestWindow() - val overlay1 = TestOverlayView.show(context) - val overlay2 = TestOverlayView.show(context) + val (overlay1, overlay2) = + getOnUiThread { + val context = overlayController.requestWindow() + Pair(TestOverlayView.show(context), TestOverlayView.show(context)) + } - overlay1.close(false) - overlay2.close(false) + runOnMainSync { + overlay1.close(false) + overlay2.close(false) + } assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse() } @Test - @UiThreadTest fun testRecreateTaskbar_closesWindow() { - TestOverlayView.show(overlayController.requestWindow()) + runOnMainSync { TestOverlayView.show(overlayController.requestWindow()) } taskbarUnitTestRule.recreateTaskbar() assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse() } @Test fun testTaskMovedToFront_closesOverlay() { - lateinit var overlay: TestOverlayView - getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) - } - + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo()) // Make sure TaskStackChangeListeners' Handler posts the callback before checking state. - getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() } + runOnMainSync { assertThat(overlay.isOpen).isFalse() } } @Test fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() { - lateinit var overlay: TestOverlayView - getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) - taskbarContext.controllers.sharedState?.allAppsVisible = false - } + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { taskbarContext.controllers.sharedState?.allAppsVisible = false } TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged() - getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isTrue() } + runOnMainSync { assertThat(overlay.isOpen).isTrue() } } @Test fun testTaskStackChanged_allAppsOpen_closesOverlay() { - lateinit var overlay: TestOverlayView - getInstrumentation().runOnMainSync { - overlay = TestOverlayView.show(overlayController.requestWindow()) - taskbarContext.controllers.sharedState?.allAppsVisible = true - } + val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) } + runOnMainSync { taskbarContext.controllers.sharedState?.allAppsVisible = true } TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged() - getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() } + runOnMainSync { assertThat(overlay.isOpen).isFalse() } } @Test - @UiThreadTest fun testUpdateLauncherDeviceProfile_overlayNotRebindSafe_closesOverlay() { - val overlayContext = overlayController.requestWindow() - val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_OPTIONS_POPUP } + val context = getOnUiThread { overlayController.requestWindow() } + val overlay = getOnUiThread { + TestOverlayView.show(context).apply { type = TYPE_OPTIONS_POPUP } + } - overlayController.updateLauncherDeviceProfile( - overlayController.launcherDeviceProfile - .toBuilder(overlayContext) - .setGestureMode(false) - .build() - ) + runOnMainSync { + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(context) + .setGestureMode(false) + .build() + ) + } assertThat(overlay.isOpen).isFalse() } @Test - @UiThreadTest fun testUpdateLauncherDeviceProfile_overlayRebindSafe_overlayStaysOpen() { - val overlayContext = overlayController.requestWindow() - val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_TASKBAR_ALL_APPS } + val context = getOnUiThread { overlayController.requestWindow() } + val overlay = getOnUiThread { + TestOverlayView.show(context).apply { type = TYPE_TASKBAR_ALL_APPS } + } - overlayController.updateLauncherDeviceProfile( - overlayController.launcherDeviceProfile - .toBuilder(overlayContext) - .setGestureMode(false) - .build() - ) + runOnMainSync { + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(context) + .setGestureMode(false) + .build() + ) + } assertThat(overlay.isOpen).isTrue() } private class TestOverlayView - private constructor( - private val overlayContext: TaskbarOverlayContext, - ) : AbstractFloatingView(overlayContext, null) { + private constructor(private val overlayContext: TaskbarOverlayContext) : + AbstractFloatingView(overlayContext, null) { var type = TYPE_OPTIONS_POPUP diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt new file mode 100644 index 0000000000..6b34cef3b1 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.rules + +import com.android.quickstep.RecentsModel +import com.android.quickstep.RecentsModel.RecentTasksChangedListener +import com.android.quickstep.TaskIconCache +import com.android.quickstep.TaskThumbnailCache +import com.android.quickstep.util.GroupTask +import java.util.function.BiConsumer +import java.util.function.Consumer +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +/** Helper class to mock the [RecentsModel] object in test */ +class MockedRecentsModelHelper { + private val mockIconCache: TaskIconCache = mock() + private val mockThumbnailCache: TaskThumbnailCache = mock() + + var taskListId = 0 + var recentTasksChangedListener: RecentTasksChangedListener? = null + var taskRequests: MutableList<(List) -> Unit> = mutableListOf() + + val mockRecentsModel: RecentsModel = mock { + on { iconCache } doReturn mockIconCache + + on { thumbnailCache } doReturn mockThumbnailCache + + on { unregisterRecentTasksChangedListener(any()) } doAnswer + { + recentTasksChangedListener = null + } + + on { registerRecentTasksChangedListener(any()) } doAnswer + { + recentTasksChangedListener = it.getArgument(0) + } + + on { getTasks(anyOrNull, Int>>(), anyOrNull()) } doAnswer + { + val request = it.getArgument, Int>?>(0) + if (request != null) { + taskRequests.add { response -> request.accept(response, taskListId) } + } + taskListId + } + + on { getTasks(anyOrNull(), anyOrNull>>()) } doAnswer + { + val request = it.getArgument>?>(1) + if (request != null) { + taskRequests.add { response -> request.accept(response) } + } + taskListId + } + + on { getTasks(anyOrNull()) } doAnswer + { + val request = it.getArgument>?>(0) + if (request != null) { + taskRequests.add { response -> request.accept(response) } + } + taskListId + } + + on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt new file mode 100644 index 0000000000..359b87618c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt @@ -0,0 +1,50 @@ +/* + * 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.launcher3.taskbar.rules + +import com.android.quickstep.util.GroupTask +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class MockedRecentsModelTestRule(private val modelHelper: MockedRecentsModelHelper) : TestRule { + private var recentTasks: List = emptyList() + + override fun apply(base: Statement?, description: Description?): Statement { + return object : Statement() { + override fun evaluate() { + base?.evaluate() + } + } + } + + // NOTE: For the update to take effect, `resolvePendingTaskRequests()` needs to be called, so + // calbacks to any pending `RecentsModel.getTasks()` get called with the updated task list. + fun updateRecentTasks(tasks: List) { + ++modelHelper.taskListId + recentTasks = tasks + modelHelper.recentTasksChangedListener?.onRecentTasksChanged() + } + + fun resolvePendingTaskRequests() { + val requests = mutableListOf<(List) -> Unit>() + requests.addAll(modelHelper.taskRequests) + modelHelper.taskRequests.clear() + + requests.forEach { it(recentTasks) } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt new file mode 100644 index 0000000000..f22580719f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt @@ -0,0 +1,85 @@ +/* + * 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.launcher3.taskbar.rules + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.NavigationMode +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.spy + +/** + * Allows tests to specify which Taskbar [Mode] to run under. + * + * [context] should match the test's target context, so that Dagger singleton instances are properly + * sandboxed. + * + * Annotate tests with [TaskbarMode] to set a mode. If the annotation is omitted for any tests, this + * rule is a no-op. + * + * Make sure this rule precedes any rules that depend on [DisplayController], or else the instance + * might be inconsistent across the test lifecycle. + */ +class TaskbarModeRule(private val context: TaskbarWindowSandboxContext) : TestRule { + /** The selected Taskbar mode. */ + enum class Mode { + TRANSIENT, + PINNED, + THREE_BUTTONS, + } + + /** Overrides Taskbar [mode] for a test. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FUNCTION) + annotation class TaskbarMode(val mode: Mode) + + override fun apply(base: Statement, description: Description): Statement { + val taskbarMode = description.getAnnotation(TaskbarMode::class.java) ?: return base + + return object : Statement() { + override fun evaluate() { + val mode = taskbarMode.mode + + getInstrumentation().runOnMainSync { + DisplayController.INSTANCE[context].let { + if (it is DisplayControllerSpy) { + it.infoModifier = { info -> + spy(info) { + on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT) + on { isPinnedTaskbar } doReturn (mode == Mode.PINNED) + on { navigationMode } doReturn + when (mode) { + Mode.TRANSIENT, + Mode.PINNED -> NavigationMode.NO_BUTTON + Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS + } + } + } + } + } + } + + base.evaluate() + } + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt new file mode 100644 index 0000000000..d4a3fa62e6 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt @@ -0,0 +1,88 @@ +/* + * 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.launcher3.taskbar.rules + +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS +import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT +import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.NavigationMode +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarModeRuleTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context) + + @Test + @TaskbarMode(TRANSIENT) + fun testTaskbarMode_transient_overridesDisplayController() { + assertThat(DisplayController.isTransientTaskbar(context)).isTrue() + assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() + assertThat(DisplayController.getNavigationMode(context)).isEqualTo(NavigationMode.NO_BUTTON) + } + + @Test + @TaskbarMode(TRANSIENT) + fun testTaskbarMode_transient_overridesDeviceProfile() { + val dp = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context) + assertThat(dp.taskbarProfile.isTransientTaskbar).isTrue() + assertThat(dp.deviceProperties.isGestureMode).isTrue() + } + + @Test + @TaskbarMode(PINNED) + fun testTaskbarMode_pinned_overridesDisplayController() { + assertThat(DisplayController.isTransientTaskbar(context)).isFalse() + assertThat(DisplayController.isPinnedTaskbar(context)).isTrue() + assertThat(DisplayController.getNavigationMode(context)).isEqualTo(NavigationMode.NO_BUTTON) + } + + @Test + @TaskbarMode(PINNED) + fun testTaskbarMode_pinned_overridesDeviceProfile() { + val dp = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context) + assertThat(dp.taskbarProfile.isTransientTaskbar).isFalse() + assertThat(dp.deviceProperties.isGestureMode).isTrue() + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testTaskbarMode_threeButtons_overridesDisplayController() { + assertThat(DisplayController.isTransientTaskbar(context)).isFalse() + assertThat(DisplayController.isPinnedTaskbar(context)).isFalse() + assertThat(DisplayController.getNavigationMode(context)) + .isEqualTo(NavigationMode.THREE_BUTTONS) + } + + @Test + @TaskbarMode(THREE_BUTTONS) + fun testTaskbarMode_threeButtons_overridesDeviceProfile() { + val dp = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context) + assertThat(dp.taskbarProfile.isTransientTaskbar).isFalse() + assertThat(dp.deviceProperties.isGestureMode).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarSandboxComponent.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarSandboxComponent.kt new file mode 100644 index 0000000000..4d84fd5002 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarSandboxComponent.kt @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.rules + +import android.content.Context +import com.android.app.displaylib.PerDisplayRepository +import com.android.launcher3.Flags +import com.android.launcher3.LauncherPrefChangeListener +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING +import com.android.launcher3.compose.core.widgetpicker.NoOpWidgetPickerModule +import com.android.launcher3.concurrent.ExecutorsModule +import com.android.launcher3.dagger.ApiWrapperModule +import com.android.launcher3.dagger.AppModule +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.BasePerDisplayModule +import com.android.launcher3.dagger.DisplayContext +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.dagger.LauncherConcurrencyModule +import com.android.launcher3.dagger.StaticObjectModule +import com.android.launcher3.dagger.WidgetModule +import com.android.launcher3.dagger.WindowContext +import com.android.launcher3.statehandlers.DesktopVisibilityController +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.FakePrefsModule +import com.android.launcher3.util.SettingsCache +import com.android.launcher3.util.dagger.LauncherExecutorsModule +import com.android.launcher3.util.window.WindowManagerProxy +import com.android.quickstep.FallbackWindowInterface +import com.android.quickstep.RecentsAnimationDeviceState +import com.android.quickstep.RotationTouchHelper +import com.android.quickstep.SystemUiProxy +import com.android.quickstep.TaskAnimationManager +import com.android.quickstep.fallback.window.RecentsWindowManager +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy + +@LauncherAppSingleton +@Component(modules = [AllTaskbarSandboxModules::class]) +interface TaskbarSandboxComponent : LauncherAppComponent { + + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder + + @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder + + override fun build(): TaskbarSandboxComponent + } +} + +@Module( + includes = + [ + ApiWrapperModule::class, + StaticObjectModule::class, + WidgetModule::class, + AppModule::class, + BasePerDisplayModule::class, + LauncherConcurrencyModule::class, + ExecutorsModule::class, + LauncherExecutorsModule::class, + FakePrefsModule::class, + DisplayControllerModule::class, + TaskbarSandboxWmProxyModule::class, + TaskbarPerDisplayReposModule::class, + DesktopVisibilityControllerModule::class, + NoOpWidgetPickerModule::class, + ] +) +interface AllTaskbarSandboxModules + +@Module +abstract class DisplayControllerModule { + @Binds abstract fun bindDisplayController(controller: DisplayControllerSpy): DisplayController +} + +/** A wrapper over display controller which allows modifying the underlying info */ +@LauncherAppSingleton +class DisplayControllerSpy +@Inject +constructor( + @ApplicationContext context: Context, + wmProxy: WindowManagerProxy, + private val prefs: LauncherPrefs, + lifecycle: DaggerSingletonTracker, +) : DisplayController(context, wmProxy, prefs, lifecycle) { + + var infoModifier: ((Info) -> Info)? = null + + // When overview on CD is enabled, DisplayController queries getInfoForDisplay instead of + // getInfo for the primary (virtual) display used in tests. So, override it to get info from the + // default display. + private val defaultInfoModifierForDisplay: ((Info?) -> Info?)? = + if (Flags.enableOverviewOnConnectedDisplays()) { + { _ -> info } + } else { + null + } + + var infoModifierForDisplay: ((Info?) -> Info?)? = defaultInfoModifierForDisplay + + private var prefListener: LauncherPrefChangeListener? = null + + init { + // When overview on CD is disabled, DisplayController only adds the info associated with + // the DEFAULT_DISPLAY. So, instead of changing the production code of DisplayController to + // use display from context we manually add the info associated with the virtual display. + if (!Flags.enableOverviewOnConnectedDisplays()) { + getOrCreatePerDisplayInfo(context.display) + lifecycle.addCloseable { removePerDisplayInfo(context.displayId) } + } + } + + override fun getInfo(): Info = infoModifier?.invoke(super.getInfo()) ?: super.getInfo() + + override fun getInfoForDisplay(displayId: Int): Info? = + infoModifierForDisplay?.invoke(super.getInfoForDisplay(displayId)) + ?: super.getInfoForDisplay(displayId) + + /** + * Sets up [TASKBAR_PINNING] pref listener for the given display. + * + *

DisplayController sets up LauncherPrefChangeListener only for the DEFAULT_DISPLAY, this is + * correct but tests rely on treating the created virtual display as default. So, instead of + * changing the production code of DisplayController to be more testable, we add a custom + * listener for our virtual display. + */ + fun setupTaskbarPinningPrefListener(displayId: Int) { + prefListener = + LauncherPrefChangeListener { notifyConfigChangeForDisplay(displayId) } + .also { prefs.addListener(it, TASKBAR_PINNING) } + } + + fun cleanup() { + prefListener?.let { prefs.removeListener(it, TASKBAR_PINNING) } + infoModifier = null + infoModifierForDisplay = defaultInfoModifierForDisplay + } +} + +/** Convenient extension to access [DisplayControllerSpy] from [TaskbarWindowSandboxContext]. */ +val TaskbarWindowSandboxContext.displayControllerSpy: DisplayControllerSpy? + get() = DisplayController.INSTANCE[this] as? DisplayControllerSpy + +@Module +object DesktopVisibilityControllerModule { + @JvmStatic + @Provides + @LauncherAppSingleton + fun provideDesktopVisibilityController( + @ApplicationContext context: Context, + systemUiProxy: SystemUiProxy, + lifecycleTracker: DaggerSingletonTracker, + ): DesktopVisibilityController { + return spy(DesktopVisibilityController(context, systemUiProxy, lifecycleTracker)) + } +} + +@Module +object TaskbarPerDisplayReposModule { + @Provides + @LauncherAppSingleton + fun provideRecentsAnimationDeviceStateRepo(): + PerDisplayRepository = mock() + + @Provides + @LauncherAppSingleton + fun provideTaskAnimationManagerRepo(): PerDisplayRepository = mock() + + @Provides + @LauncherAppSingleton + fun provideRotationTouchHandlerRepo(): PerDisplayRepository = mock() + + @Provides + @LauncherAppSingleton + fun provideFallbackWindowInterfaceRepo(): PerDisplayRepository = mock() + + @Provides + @LauncherAppSingleton + fun provideRecentsWindowManagerRepo(): PerDisplayRepository = mock() + + @Provides + @LauncherAppSingleton + @DisplayContext + fun provideDisplayContext(): PerDisplayRepository = mock() + + @Provides + @LauncherAppSingleton + @WindowContext + fun provideWindowContext(): PerDisplayRepository = mock() +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt new file mode 100644 index 0000000000..9ac19e1392 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt @@ -0,0 +1,257 @@ +/* + * 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.launcher3.taskbar.rules + +import android.app.Instrumentation +import android.app.PendingIntent +import android.content.IIntentSender +import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE +import android.provider.Settings.Secure.USER_SETUP_COMPLETE +import android.provider.Settings.Secure.getUriFor +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.LauncherAppState +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarControllers +import com.android.launcher3.taskbar.TaskbarManagerImpl +import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks +import com.android.launcher3.taskbar.TaskbarUIController +import com.android.launcher3.taskbar.bubbles.BubbleControllers +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.TestUtil +import com.android.launcher3.util.coroutines.ProductionDispatchers +import com.android.quickstep.AllAppsActionManager +import com.android.quickstep.LauncherDisplaysWithDecorationsRepositoryCompat +import com.android.quickstep.fallback.window.RecentsWindowManager +import com.android.quickstep.input.QuickstepKeyGestureEventsManager +import java.lang.reflect.Field +import java.lang.reflect.ParameterizedType +import java.util.Locale +import java.util.Optional +import org.junit.Assume.assumeTrue +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +/** + * Manages the Taskbar lifecycle for unit tests. + * + * Tests should pass in themselves as [testInstance]. They also need to provide their target + * [context] through the constructor. + * + * See [InjectController] for grabbing controller(s) under test with minimal boilerplate. + * + * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is + * that code that is executed on the main thread in production should also happen on that thread + * when tested. + * + * `@UiThreadTest` is incompatible with this rule. The annotation causes this rule to run on the + * main thread, but it needs to be run on the test thread for it to work properly. Instead, only run + * code that requires the main thread using something like [Instrumentation.runOnMainSync] or + * [TestUtil.getOnUiThread]. + * + * ``` + * @Test + * fun example() { + * instrumentation.runOnMainSync { doWorkThatPostsMessage() } + * // Second lambda will not execute until message is processed. + * instrumentation.runOnMainSync { verifyMessageResults() } + * } + * ``` + */ +class TaskbarUnitTestRule( + private val testInstance: Any, + private val context: TaskbarWindowSandboxContext, + private val controllerInjectionCallback: () -> Unit = {}, +) : TestRule { + + private val instrumentation = InstrumentationRegistry.getInstrumentation() + + private lateinit var taskbarManager: TaskbarManagerImpl + + val activityContext: TaskbarActivityContext + get() { + return taskbarManager.currentActivityContext + ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.") + } + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + + // Only run test when Taskbar is enabled. + instrumentation.runOnMainSync { + assumeTrue( + LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent + ) + } + + // Process secure setting annotations. + context.settingsCacheSandbox[getUriFor(USER_SETUP_COMPLETE)] = + if (description.getAnnotation(UserSetupMode::class.java) != null) 0 else 1 + context.settingsCacheSandbox[getUriFor(NAV_BAR_KIDS_MODE)] = + if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0 + + val quickstepKeyGestureEventsManagerSpy = + spy(QuickstepKeyGestureEventsManager(context)) + doNothing() + .whenever(quickstepKeyGestureEventsManagerSpy) + .registerAllAppsKeyGestureEvent(any()) + doNothing() + .whenever(quickstepKeyGestureEventsManagerSpy) + .unregisterAllAppsKeyGestureEvent() + doNothing() + .whenever(quickstepKeyGestureEventsManagerSpy) + .registerOverviewKeyGestureEvent(any()) + doNothing() + .whenever(quickstepKeyGestureEventsManagerSpy) + .unregisterOverviewKeyGestureEvent() + taskbarManager = + TestUtil.getOnUiThread { + object : + TaskbarManagerImpl( + context, + AllAppsActionManager( + context, + UI_HELPER_EXECUTOR, + quickstepKeyGestureEventsManagerSpy, + ) { + PendingIntent(IIntentSender.Default()) + }, + object : TaskbarNavButtonCallbacks {}, + RecentsWindowManager.REPOSITORY_INSTANCE.get(context), + LauncherDisplaysWithDecorationsRepositoryCompat.INSTANCE.get( + context + ), + ProductionDispatchers.main, + ) { + override fun recreateTaskbars() { + super.recreateTaskbars() + if (currentActivityContext != null) { + injectControllers() + // TODO(b/346394875): we should test a non-default uiController. + activityContext.setUIController(TaskbarUIController.DEFAULT) + controllerInjectionCallback.invoke() + } + } + + override fun recreateTaskbarForDisplay(displayId: Int, duration: Int) { + super.recreateTaskbarForDisplay(displayId, duration) + if ( + displayId == context.displayId && currentActivityContext != null + ) { + injectControllers() + // TODO(b/346394875): we should test a non-default uiController. + activityContext.setUIController(TaskbarUIController.DEFAULT) + controllerInjectionCallback.invoke() + } + } + } + } + + if (description.getAnnotation(ForceRtl::class.java) != null) { + // Needs to be set on window context instead of sandbox context, because it does + // does not propagate between them. However, this change will impact created + // TaskbarActivityContext instances, since they wrap the window context. + // TODO: iterate through all window contexts and do this. + taskbarManager.primaryWindowContext.resources.configuration.setLayoutDirection( + RTL_LOCALE + ) + } + + try { + // Required to complete initialization. + instrumentation.runOnMainSync { taskbarManager.onUserUnlocked() } + + base.evaluate() + } finally { + instrumentation.runOnMainSync { taskbarManager.destroy() } + context.displayControllerSpy?.cleanup() + } + } + } + } + + /** Simulates Taskbar recreation lifecycle. */ + fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbars() } + + private fun injectControllers() { + val bubbleControllerTypes = + BubbleControllers::class.java.fields.map { f -> + if (f.type == Optional::class.java) { + (f.genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + } else { + f.type + } + } + testInstance.javaClass.fields + .filter { it.isAnnotationPresent(InjectController::class.java) } + .forEach { + val controllers: Any = + if (it.type in bubbleControllerTypes) { + activityContext.controllers.bubbleControllers.orElseThrow { + NoSuchElementException("Bubble controllers are not initialized") + } + } else { + activityContext.controllers + } + injectController(it, testInstance, controllers) + } + } + + private fun injectController(field: Field, testInstance: Any, controllers: Any) { + val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type } + field.set( + testInstance, + controllerFieldsByType[field.type]?.get(controllers) + ?: throw NoSuchElementException("Failed to find controller for ${field.type}"), + ) + } + + /** + * Annotates test controller fields to inject the corresponding controllers from the current + * [TaskbarControllers] instance. + * + * Controllers are injected during test setup and upon calling [recreateTaskbar]. + * + * Multiple controllers can be injected if needed. + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FIELD) + annotation class InjectController + + /** Overrides [USER_SETUP_COMPLETE] to be `false` for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class UserSetupMode + + /** Overrides [NAV_BAR_KIDS_MODE] to be `true` for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class NavBarKidsMode + + /** Forces RTL UI for tests. */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) + annotation class ForceRtl +} + +private val RTL_LOCALE = Locale.of("ar", "XB") diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt new file mode 100644 index 0000000000..b8b0b5d1e1 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt @@ -0,0 +1,238 @@ +/* + * 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.launcher3.taskbar.rules + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.Utilities +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarKeyguardController +import com.android.launcher3.taskbar.TaskbarManager +import com.android.launcher3.taskbar.TaskbarStashController +import com.android.launcher3.taskbar.bubbles.BubbleBarController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.wm.shell.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Rule +import org.junit.Test +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.model.Statement + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarUnitTestRuleTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val setFlagsRule = SetFlagsRule() + + @Test + fun testSetup_taskbarInitialized() { + onSetup { assertThat(activityContext).isInstanceOf(TaskbarActivityContext::class.java) } + } + + @Test + fun testRecreateTaskbar_activityContextChanged() { + onSetup { + val context1 = activityContext + recreateTaskbar() + val context2 = activityContext + assertThat(context1).isNotSameInstanceAs(context2) + } + } + + @Test + fun testTeardown_taskbarDestroyed() { + val testRule = TaskbarUnitTestRule(this, context) + testRule.apply(EMPTY_STATEMENT, DESCRIPTION).evaluate() + assertThrows(RuntimeException::class.java) { testRule.activityContext } + } + + @Test + fun testInjectController_validControllerType_isInjected() { + val testClass = + object { + @InjectController lateinit var controller: TaskbarStashController + val isInjected: Boolean + get() = ::controller.isInitialized + } + + TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate() + + onSetup(TaskbarUnitTestRule(testClass, context)) { + assertThat(testClass.isInjected).isTrue() + } + } + + @Test + fun testInjectController_multipleControllers_areInjected() { + val testClass = + object { + @InjectController lateinit var controller1: TaskbarStashController + @InjectController lateinit var controller2: TaskbarKeyguardController + val areInjected: Boolean + get() = ::controller1.isInitialized && ::controller2.isInitialized + } + + onSetup(TaskbarUnitTestRule(testClass, context)) { + assertThat(testClass.areInjected).isTrue() + } + } + + @Test + fun testInjectController_invalidControllerType_exceptionThrown() { + val testClass = + object { + @InjectController lateinit var manager: TaskbarManager // Not a controller. + } + + // We cannot use #assertThrows because we also catch an assumption violated exception when + // running #evaluate on devices that do not support Taskbar. + val result = + try { + TaskbarUnitTestRule(testClass, context) + .apply(EMPTY_STATEMENT, DESCRIPTION) + .evaluate() + } catch (e: NoSuchElementException) { + e + } + assertThat(result).isInstanceOf(NoSuchElementException::class.java) + } + + @Test + fun testInjectController_recreateTaskbar_controllerChanged() { + val testClass = + object { + @InjectController lateinit var controller: TaskbarStashController + } + + onSetup(TaskbarUnitTestRule(testClass, context)) { + val controller1 = testClass.controller + recreateTaskbar() + val controller2 = testClass.controller + assertThat(controller1).isNotSameInstanceAs(controller2) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR) + @Test + fun testInjectBubbleController_bubbleFlagOn_isInjected() { + val testClass = + object { + @InjectController lateinit var controller: BubbleBarController + val isInjected: Boolean + get() = ::controller.isInitialized + } + + TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate() + + onSetup(TaskbarUnitTestRule(testClass, context)) { + assertThat(testClass.isInjected).isTrue() + } + } + + @DisableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR) + @Test + fun testInjectBubbleController_bubbleFlagOff_exceptionThrown() { + val testClass = + object { + @InjectController lateinit var controller: BubbleBarController + } + + // We cannot use #assertThrows because we also catch an assumption violated exception when + // running #evaluate on devices that do not support Taskbar. + val result = + try { + TaskbarUnitTestRule(testClass, context) + .apply(EMPTY_STATEMENT, DESCRIPTION) + .evaluate() + } catch (e: NoSuchElementException) { + e + } + assertThat(result).isInstanceOf(NoSuchElementException::class.java) + } + + @Test + fun testUserSetupMode_default_isComplete() { + onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() } + } + + @Test + fun testUserSetupMode_withAnnotation_isIncomplete() { + @UserSetupMode class Mode + onSetup(description = Description.createSuiteDescription(Mode::class.java)) { + assertThat(activityContext.isUserSetupComplete).isFalse() + } + } + + @Test + fun testNavBarKidsMode_default_navBarNotForcedVisible() { + onSetup { assertThat(activityContext.isNavBarForceVisible).isFalse() } + } + + @Test + fun testNavBarKidsMode_withAnnotation_navBarForcedVisible() { + @NavBarKidsMode class Mode + onSetup(description = Description.createSuiteDescription(Mode::class.java)) { + assertThat(activityContext.isNavBarForceVisible).isTrue() + } + } + + @Test + fun testForceRtlAnnotation_setsActivityContextLayoutDirection() { + @ForceRtl class Rtl + onSetup(description = Description.createSuiteDescription(Rtl::class.java)) { + assertThat(Utilities.isRtl(activityContext.resources)).isTrue() + } + } + + /** + * Executes [runTest] after the [testRule] setup phase completes. + * + * A [description] can also be provided to mimic annotating a test or test class. + */ + private fun onSetup( + testRule: TaskbarUnitTestRule = TaskbarUnitTestRule(this, context), + description: Description = DESCRIPTION, + runTest: TaskbarUnitTestRule.() -> Unit, + ) { + testRule + .apply( + object : Statement() { + override fun evaluate() = runTest(testRule) + }, + description, + ) + .evaluate() + } + + private companion object { + private val EMPTY_STATEMENT = + object : Statement() { + override fun evaluate() = Unit + } + private val DESCRIPTION = + Description.createSuiteDescription(TaskbarUnitTestRuleTest::class.java) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt new file mode 100644 index 0000000000..64a6902c38 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt @@ -0,0 +1,123 @@ +/* + * 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.launcher3.taskbar.rules + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.view.Display +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.core.app.ApplicationProvider +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.SettingsCacheSandbox +import com.android.quickstep.SystemUiProxy +import org.junit.rules.ExternalResource +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import org.mockito.kotlin.whenever + +/** + * [SandboxApplication] for running Taskbar tests. + * + * Tests need to run on a [VirtualDisplay] to avoid conflicting with Launcher's Taskbar on the + * [DEFAULT_DISPLAY] (i.e. test is executing on a device). + */ +class TaskbarWindowSandboxContext +private constructor( + private val base: SandboxApplication, + val virtualDisplay: VirtualDisplay, + private val params: SandboxParams, +) : ContextWrapper(base), TestRule { + + val settingsCacheSandbox = SettingsCacheSandbox() + + private val virtualDisplayRule = + object : ExternalResource() { + override fun after() = virtualDisplay.release() + } + + // Filter out DEFAULT_DISPLAY in case code accesses displays property. The primary virtual + // display has a different ID. + private val sandboxDisplayManagerRule = + object : ExternalResource() { + override fun before() { + val dm = base.spyService(DisplayManager::class.java) + whenever(dm.displays).thenAnswer { i -> + @Suppress("UNCHECKED_CAST") + val displays = i.callRealMethod() as? Array ?: emptyArray() + displays.filter { it.displayId != DEFAULT_DISPLAY }.toTypedArray() + } + } + } + + private val singletonSetupRule = + object : ExternalResource() { + override fun before() { + val context = this@TaskbarWindowSandboxContext + val builder = + params.builderBase + .bindSystemUiProxy(params.systemUiProxyProvider.invoke(context)) + .bindSettingsCache(settingsCacheSandbox.cache) + base.initDaggerComponent(builder) + } + } + + override fun apply(statement: Statement, description: Description): Statement { + return RuleChain.outerRule(virtualDisplayRule) + .around(base) + .around(sandboxDisplayManagerRule) + .around(singletonSetupRule) + .apply(statement, description) + } + + companion object { + private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay" + + /** Creates a [SandboxApplication] for Taskbar tests. */ + fun create(params: SandboxParams = SandboxParams()): TaskbarWindowSandboxContext { + val base = ApplicationProvider.getApplicationContext() + val displayManager = checkNotNull(base.getSystemService(DisplayManager::class.java)) + // Create virtual display to avoid clashing with Taskbar on default display. + val virtualDisplay = + base.resources.displayMetrics.let { + displayManager.createVirtualDisplay( + VIRTUAL_DISPLAY_NAME, + it.widthPixels, + it.heightPixels, + it.densityDpi, + /* surface= */ null, + /* flags= */ 0, + ) + } + + return TaskbarWindowSandboxContext( + SandboxApplication(base = base.createDisplayContext(virtualDisplay.display)), + virtualDisplay, + params, + ) + } + } +} + +/** Include additional bindings when building a [TaskbarSandboxComponent]. */ +data class SandboxParams( + val systemUiProxyProvider: (Context) -> SystemUiProxy = { SystemUiProxy(it) }, + val builderBase: TaskbarSandboxComponent.Builder = DaggerTaskbarSandboxComponent.builder(), +) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt new file mode 100644 index 0000000000..69095e7e75 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt @@ -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. + */ + +package com.android.launcher3.taskbar.rules + +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.model.Statement + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023"]) +class TaskbarWindowSandboxContextTest { + + @Test + fun testVirtualDisplay_releasedOnTeardown() { + val context = TaskbarWindowSandboxContext.create() + assertThat(context.virtualDisplay.token).isNotNull() + + context + .apply( + object : Statement() { + override fun evaluate() = Unit + }, + Description.createSuiteDescription(TaskbarWindowSandboxContextTest::class.java), + ) + .evaluate() + + assertThat(context.virtualDisplay.token).isNull() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt new file mode 100644 index 0000000000..52238c83d9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt @@ -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 com.android.launcher3.util + +import android.net.Uri +import com.android.launcher3.util.SettingsCache.OnChangeListener +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Provides [SettingsCache] sandboxed from system settings for testing. */ +class SettingsCacheSandbox { + private val values = mutableMapOf() + private val listeners = mutableMapOf>() + + /** + * Fake cache that delegates: + * - [SettingsCache.getValue] to [values] + * - [SettingsCache.mListenerMap] to [listeners]. + */ + val cache = + mock { + on { getValue(any()) } doAnswer { mock.getValue(it.getArgument(0), 1) } + on { getValue(any(), any()) } doAnswer + { + values.getOrDefault(it.getArgument(0), it.getArgument(1)) == 1 + } + + doAnswer { + listeners.getOrPut(it.getArgument(0)) { mutableSetOf() }.add(it.getArgument(1)) + } + .whenever(mock) + .register(any(), any()) + doAnswer { listeners[it.getArgument(0)]?.remove(it.getArgument(1)) } + .whenever(mock) + .unregister(any(), any()) + } + + operator fun get(key: Uri): Int? = values[key] + + operator fun set(key: Uri, value: Int) { + if (value == values[key]) return + values[key] = value + listeners[key]?.forEach { it.onSettingsChanged(value == 1) } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt new file mode 100644 index 0000000000..9b0a95a526 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widget.picker + +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD +import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class WidgetCategoryFilterTest { + + @Test + fun filterValueZero_everythingMatches() { + val noFilter = WidgetCategoryFilter(categoryMask = 0) + + noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + noFilter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + noFilter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD) + noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX) + noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD) + noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_NOT_KEYGUARD) + noFilter.assertMatches( + WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD + ) + noFilter.assertMatches( + WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_NOT_KEYGUARD + ) + noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + } + + @Test + fun includeHomeScreen_matchesOnlyIfHomeScreenExists() { + val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN) + + filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX) + filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX) + } + + @Test + fun includeHomeScreenOrKeyguard_matchesIfEitherHomeScreenOrKeyguardExists() { + val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD) + + filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD) + } + + @Test + fun excludeNotKeyguard_doesNotMatchIfNotKeyguardExists() { + val filter = WidgetCategoryFilter(WIDGET_CATEGORY_NOT_KEYGUARD.inv()) + + filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch( + WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN + ) + } + + @Test + fun multipleExclusions_doesNotMatchIfExcludedCategoriesExist() { + val filter = + WidgetCategoryFilter( + WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv() + ) + + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX) + filter.assertMatches(WIDGET_CATEGORY_KEYGUARD) + filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN) + + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN) + filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD) + filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD) + filter.assertDoesNotMatch( + WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN + ) + } + + private fun WidgetCategoryFilter.assertMatches(category: Int) { + assertThat(matches(category)).isTrue() + } + + private fun WidgetCategoryFilter.assertDoesNotMatch(category: Int) { + assertThat(matches(category)).isFalse() + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java new file mode 100644 index 0000000000..ec49237e96 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java @@ -0,0 +1,514 @@ +/* + * 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.quickstep; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; +import static com.android.launcher3.statehandlers.DesktopVisibilityController.INACTIVE_DESK_ID; +import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; +import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.ViewTreeObserver; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherRootView; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.statemanager.BaseState; +import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.statemanager.StatefulContainer; +import com.android.launcher3.util.MSDLPlayerWrapper; +import com.android.launcher3.util.SandboxApplication; +import com.android.launcher3.util.SystemUiController; +import com.android.quickstep.util.ContextInitListener; +import com.android.quickstep.util.MotionPauseDetector; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.Flags; +import com.android.systemui.shared.system.InputConsumerController; +import com.android.wm.shell.shared.split.SplitBounds; + +import com.google.android.msdl.data.model.MSDLToken; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Collections; +import java.util.HashMap; + +public abstract class AbsSwipeUpHandlerTestCase< + STATE_TYPE extends BaseState, + RECENTS_CONTAINER extends Context & RecentsViewContainer & StatefulContainer, + RECENTS_VIEW extends RecentsView, + SWIPE_HANDLER extends AbsSwipeUpHandler, + CONTAINER_INTERFACE extends BaseContainerInterface> { + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Rule + public final SandboxApplication mContext = new SandboxApplication(); + + protected final InputConsumerController mInputConsumerController = + InputConsumerController.getRecentsAnimationInputConsumer(); + protected final ActivityManager.RunningTaskInfo mRunningTaskInfo = + new ActivityManager.RunningTaskInfo(); + protected final TopTaskTracker.CachedTaskInfo mCachedTaskInfo = + new TopTaskTracker.CachedTaskInfo( + Collections.singletonList(mRunningTaskInfo), mContext, DEFAULT_DISPLAY, + INACTIVE_DESK_ID); + protected final RemoteAnimationTarget mRemoteAnimationTarget = new RemoteAnimationTarget( + /* taskId= */ 0, + /* mode= */ RemoteAnimationTarget.MODE_CLOSING, + /* leash= */ new SurfaceControl(), + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + new Configuration().windowConfiguration, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + /* taskInfo= */ mRunningTaskInfo, + /* allowEnterPip= */ false); + + protected final RemoteAnimationTarget mRemoteAnimationLeftTop = new RemoteAnimationTarget( + /* taskId= */ 1, + /* mode= */ RemoteAnimationTarget.MODE_CLOSING, + /* leash= */ new SurfaceControl(), + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + new Configuration().windowConfiguration, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + /* taskInfo= */ mRunningTaskInfo, + /* allowEnterPip= */ false); + + protected final RemoteAnimationTarget mRemoteAnimationRightBottom = new RemoteAnimationTarget( + /* taskId= */ 2, + /* mode= */ RemoteAnimationTarget.MODE_CLOSING, + /* leash= */ new SurfaceControl(), + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + new Configuration().windowConfiguration, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + /* taskInfo= */ mRunningTaskInfo, + /* allowEnterPip= */ false); + + protected RecentsAnimationTargets mRecentsAnimationTargets; + protected TaskAnimationManager mTaskAnimationManager; + protected StateManager mStateManager; + + @Mock protected CONTAINER_INTERFACE mActivityInterface; + @Mock protected ContextInitListener mContextInitListener; + @Mock protected RecentsAnimationController mRecentsAnimationController; + @Mock protected STATE_TYPE mState; + @Mock protected ViewTreeObserver mViewTreeObserver; + @Mock protected DragLayer mDragLayer; + @Mock protected LauncherRootView mRootView; + @Mock protected SystemUiController mSystemUiController; + @Mock protected GestureState mGestureState; + @Mock protected MSDLPlayerWrapper mMSDLPlayerWrapper; + @Mock protected RecentsAnimationDeviceState mDeviceState; + @Mock protected RotationTouchHelper mRotationTouchHelper; + @Mock protected StateManager.AtomicAnimationFactory mAtomicAnimationFactory; + + @Before + public void setUpAnimationTargets() { + Bundle extras = new Bundle(); + extras.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, true); + extras.putParcelable(KEY_EXTRA_SPLIT_BOUNDS, new SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ mRemoteAnimationLeftTop.taskId, + /* rightBottomTaskId = */ mRemoteAnimationRightBottom.taskId, + /* snapPosition = */ SNAP_TO_2_50_50)); + mRecentsAnimationTargets = new RecentsAnimationTargets( + new RemoteAnimationTarget[] {mRemoteAnimationLeftTop}, + new RemoteAnimationTarget[] {mRemoteAnimationRightBottom}, + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + /* homeContentInsets= */ new Rect(), + /* minimizedHomeBounds= */ null, + extras); + } + + @Before + public void setUpRunningTaskInfo() { + mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Before + public void setUpGestureState() { + when(mGestureState.getRunningTask()).thenReturn(mCachedTaskInfo); + when(mGestureState.getLastAppearedTaskIds()).thenReturn(new int[0]); + when(mGestureState.getLastStartedTaskIds()).thenReturn(new int[1]); + when(mGestureState.getHomeIntent()).thenReturn(new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + doReturn(mActivityInterface).when(mGestureState).getContainerInterface(); + } + + @Before + public void setUpRecentsView() { + RECENTS_VIEW recentsView = getRecentsView(); + when(recentsView.getViewTreeObserver()).thenReturn(mViewTreeObserver); + doAnswer(answer -> { + runOnMainSync(() -> answer.getArgument(0).run()); + return this; + }).when(recentsView).runOnPageScrollsInitialized(any()); + } + + @Before + public void setUpRecentsContainer() { + mTaskAnimationManager = spy(new TaskAnimationManager(mContext, DEFAULT_DISPLAY)); + RECENTS_CONTAINER recentsContainer = getRecentsContainer(); + RECENTS_VIEW recentsView = getRecentsView(); + + when(recentsContainer.getDeviceProfile()).thenReturn(new DeviceProfile()); + when(recentsContainer.getOverviewPanel()).thenReturn(recentsView); + when(recentsContainer.getDragLayer()).thenReturn(mDragLayer); + when(recentsContainer.getRootView()).thenReturn(mRootView); + when(recentsContainer.getSystemUiController()).thenReturn(mSystemUiController); + when(recentsContainer.createAtomicAnimationFactory()).thenReturn(mAtomicAnimationFactory); + when(mActivityInterface.createActivityInitListener(any())) + .thenReturn(mContextInitListener); + doReturn(recentsContainer).when(mActivityInterface).getCreatedContainer(); + doAnswer(answer -> { + answer.getArgument(0).run(); + return this; + }).when(recentsContainer).runOnBindToTouchInteractionService(any()); + + mStateManager = spy(new StateManager<>(recentsContainer, getBaseState())); + + doReturn(mStateManager).when(recentsContainer).getStateManager(); + } + + @Test + public void testInitWhenReady_registersActivityInitListener() { + String reasonString = "because i said so"; + + createSwipeHandler().initWhenReady(reasonString); + verify(mContextInitListener).register(eq(reasonString)); + } + + @Test + public void testOnRecentsAnimationCanceled_unregistersActivityInitListener() { + createSwipeHandler() + .onRecentsAnimationCanceled(new HashMap<>()); + + runOnMainSync(() -> verify(mContextInitListener) + .unregister(eq("AbsSwipeUpHandler.onRecentsAnimationCanceled"))); + } + + @Test + public void testOnConsumerAboutToBeSwitched_unregistersActivityInitListener() { + createSwipeHandler().onConsumerAboutToBeSwitched(); + + runOnMainSync(() -> verify(mContextInitListener) + .unregister("AbsSwipeUpHandler.invalidateHandler")); + } + + @Test + public void testOnConsumerAboutToBeSwitched_midQuickSwitch_unregistersActivityInitListener() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.NEW_TASK) + .onConsumerAboutToBeSwitched(); + + runOnMainSync(() -> verify(mContextInitListener) + .unregister(eq("AbsSwipeUpHandler.cancelCurrentAnimation"))); + } + + @Test + public void testStartNewTask_finishesRecentsAnimationController() { + SWIPE_HANDLER absSwipeUpHandler = createSwipeHandler(); + + onRecentsAnimationStart(absSwipeUpHandler); + + runOnMainSync(() -> { + absSwipeUpHandler.startNewTask(unused -> {}); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @Test + public void testHomeGesture_finishesRecentsAnimationController() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController).detachNavigationBarFromApp(true); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @EnableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED}) + @Test + public void testHomeGesture_handsOffAnimation() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController).handOffAnimation(any(), any()); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @DisableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED}) + @Test + public void testHomeGesture_doesNotHandOffAnimation_withFlagsDisabled() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController, never()).handOffAnimation(any(), any()); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @Test + public void testHomeGesture_invalidatesHandlerAfterParallelAnim() { + ValueAnimator parallelAnim = new ValueAnimator(); + parallelAnim.setRepeatCount(ValueAnimator.INFINITE); + when(mActivityInterface.getParallelAnimationToGestureEndTarget(any(), anyLong(), any())) + .thenReturn(parallelAnim); + SWIPE_HANDLER handler = createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + runOnMainSync(() -> { + parallelAnim.start(); + verifyRecentsAnimationFinishedAndCallCallback(); + assertFalse(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)); + parallelAnim.end(); + assertTrue(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)); + }); + } + + @Test + public void testHomeGesture_invalidatesHandlerIfNoParallelAnim() { + when(mActivityInterface.getParallelAnimationToGestureEndTarget(any(), anyLong(), any())) + .thenReturn(null); + SWIPE_HANDLER handler = createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + runOnMainSync(() -> { + verifyRecentsAnimationFinishedAndCallCallback(); + assertTrue(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)); + }); + } + + @Test + @Ignore("b/418979038") + public void invalidateHandlerWithLauncher_runsGestureAnimationEndCallback() { + SWIPE_HANDLER handler = createSwipeHandler(); + Runnable onGestureAnimationEndCallback = mock(Runnable.class); + handler.setGestureAnimationEndCallback(onGestureAnimationEndCallback); + handler.onActivityInit(true); // Sets STATE_LAUNCHER_PRESENT + + // Use onConsumerAboutToBeSwitched to call reset(),to sets STATE_HANDLER_INVALIDATED. This + // will then call invalidateHandlerWithLauncher. This will hit the reset() in the else + // condition of onConsumerAboutToBeSwitched, as the gesture state is a mock and will + // return false for the booleans checked in the if-condition. + handler.onConsumerAboutToBeSwitched(); + + verify(getRecentsView()).onGestureAnimationEnd(); + verify(onGestureAnimationEndCallback).run(); + } + + @Test + @EnableFlags(com.android.launcher3.Flags.FLAG_MSDL_FEEDBACK) + public void onMotionPauseDetected_playsSwipeThresholdToken() { + SWIPE_HANDLER handler = createSwipeHandler(); + MotionPauseDetector.OnMotionPauseListener listener = handler.getMotionPauseListener(); + listener.onMotionPauseDetected(); + + verify(mMSDLPlayerWrapper, times(1)).playToken(eq(MSDLToken.SWIPE_THRESHOLD_INDICATOR)); + verifyNoMoreInteractions(mMSDLPlayerWrapper); + } + + @Test + public void testOnContainerDestroy_cleansUpSwipeHandler() { + SWIPE_HANDLER swipeHandler = createSwipeHandler(); + + swipeHandler.onActivityInit(true); + + RECENTS_CONTAINER container = getRecentsContainer(); + ArgumentCaptor onContainerDestroyCallbackCaptor = + ArgumentCaptor.forClass(Runnable.class); + + verify(container) + .addEventCallback(eq(EVENT_DESTROYED), onContainerDestroyCallbackCaptor.capture()); + + assertNotNull(swipeHandler.mRecentsView); + assertNotNull(swipeHandler.mContainer); + + onContainerDestroyCallbackCaptor.getValue().run(); + + assertNull(swipeHandler.mRecentsView); + assertNull(swipeHandler.mContainer); + verify(mTaskAnimationManager).onLauncherDestroyed(); + runOnMainSync(() -> verify(mContextInitListener) + .unregister(eq("AbsSwipeUpHandler.mLauncherOnDestroyCallback"))); + } + + @Test + public void test_noActivityInit_doesNotThrowException() { + // Do not trigger onActivityInit to ensure AbsSwipeUpHandler.mRecentsView and + // AbsSwipeUpHandler.mContainer are null + createSwipeUpHandlerForGesture( + GestureState.GestureEndTarget.HOME, /* triggerOnActivityInit= */ false); + } + + /** + * Verifies that RecentsAnimationController#finish() is called, and captures and runs any + * callback that was passed to it. This ensures that STATE_CURRENT_TASK_FINISHED is correctly + * set for example. + */ + private void verifyRecentsAnimationFinishedAndCallCallback() { + ArgumentCaptor finishCallback = ArgumentCaptor.forClass(Runnable.class); + // Check if the 2 parameter method is called. + verify(mRecentsAnimationController, atLeast(0)).finish( + anyBoolean(), finishCallback.capture()); + if (finishCallback.getAllValues().isEmpty()) { + // Check if the 3 parameter method is called. + verify(mRecentsAnimationController).finish( + anyBoolean(), finishCallback.capture(), anyBoolean()); + } + if (finishCallback.getValue() != null) { + finishCallback.getValue().run(); + } + } + + private SWIPE_HANDLER createSwipeUpHandlerForGesture(GestureState.GestureEndTarget endTarget) { + return createSwipeUpHandlerForGesture(endTarget, true); + } + + private SWIPE_HANDLER createSwipeUpHandlerForGesture( + GestureState.GestureEndTarget endTarget, boolean triggerOnActivityInit) { + boolean isQuickSwitch = endTarget == GestureState.GestureEndTarget.NEW_TASK; + + doReturn(mState).when(mActivityInterface).stateFromGestureEndTarget(any()); + + SWIPE_HANDLER swipeHandler = createSwipeHandler(SystemClock.uptimeMillis(), isQuickSwitch); + + if (triggerOnActivityInit) { + swipeHandler.onActivityInit(/* alreadyOnHome= */ false); + } + swipeHandler.onGestureStarted(isQuickSwitch); + onRecentsAnimationStart(swipeHandler); + + when(mGestureState.getRunningTaskIds(anyBoolean())).thenReturn(new int[0]); + runOnMainSync(swipeHandler::switchToScreenshot); + + when(mGestureState.getEndTarget()).thenReturn(endTarget); + when(mGestureState.isRecentsAnimationRunning()).thenReturn(isQuickSwitch); + float xVelocityPxPerMs = isQuickSwitch ? 100 : 0; + float yVelocityPxPerMs = isQuickSwitch ? 0 : -100; + swipeHandler.onGestureEnded( + yVelocityPxPerMs, new PointF(xVelocityPxPerMs, yVelocityPxPerMs), isQuickSwitch); + swipeHandler.onCalculateEndTarget(); + runOnMainSync(swipeHandler::onSettledOnEndTarget); + + return swipeHandler; + } + + private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) { + runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart( + mRecentsAnimationController, mRecentsAnimationTargets, /* transitionInfo= */ null)); + } + + protected static void runOnMainSync(Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } + + @NonNull + private SWIPE_HANDLER createSwipeHandler() { + return createSwipeHandler(SystemClock.uptimeMillis(), false); + } + + @NonNull + protected abstract SWIPE_HANDLER createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture); + + @NonNull + protected abstract RECENTS_CONTAINER getRecentsContainer(); + + @NonNull + protected abstract RECENTS_VIEW getRecentsView(); + + @NonNull + protected abstract STATE_TYPE getBaseState(); +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt index 73b35e8a5c..acfa9b40eb 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt @@ -18,44 +18,88 @@ package com.android.quickstep import android.app.PendingIntent import android.content.IIntentSender +import android.hardware.input.InputManager +import android.provider.Settings +import android.provider.Settings.Secure.USER_SETUP_COMPLETE import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.AllModulesForTest import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.SettingsCache +import com.android.launcher3.util.SettingsCacheSandbox import com.android.launcher3.util.TestUtil +import com.android.quickstep.input.QuickstepKeyGestureEventsManager import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit.SECONDS +import org.junit.After +import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.whenever private const val TIMEOUT = 5L +private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPLETE) @RunWith(AndroidJUnit4::class) class AllAppsActionManagerTest { private val callbackSemaphore = Semaphore(0) private val bgExecutor = UI_HELPER_EXECUTOR - private val allAppsActionManager = - AllAppsActionManager( - InstrumentationRegistry.getInstrumentation().targetContext, - bgExecutor, - ) { - callbackSemaphore.release() - PendingIntent(IIntentSender.Default()) + @get:Rule val context = SandboxApplication() + private val inputManager = context.spyService(InputManager::class.java) + + private val settingsCacheSandbox = + SettingsCacheSandbox().also { it[USER_SETUP_COMPLETE_URI] = 1 } + private val quickstepKeyGestureEventsManager by + lazy(LazyThreadSafetyMode.NONE) { spy(QuickstepKeyGestureEventsManager(context)) } + + private val allAppsActionManager by + lazy(LazyThreadSafetyMode.NONE) { + AllAppsActionManager(context, bgExecutor, quickstepKeyGestureEventsManager) { + callbackSemaphore.release() + PendingIntent(IIntentSender.Default()) + } } + @Before + fun setUp() { + context.initDaggerComponent( + DaggerAllAppsActionManagerTestComponent.builder() + .bindSettingsCache(settingsCacheSandbox.cache) + ) + + doNothing().whenever(inputManager).registerKeyGestureEventHandler(any(), any()) + doNothing().whenever(inputManager).unregisterKeyGestureEventHandler(any()) + } + + @After fun destroyManager() = allAppsActionManager.onDestroy() + @Test fun taskbarPresent_actionRegistered() { allAppsActionManager.isTaskbarPresent = true + TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register. assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() assertThat(allAppsActionManager.isActionRegistered).isTrue() + verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any()) } @Test fun homeAndOverviewSame_actionRegistered() { allAppsActionManager.isHomeAndOverviewSame = true + TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register. assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() assertThat(allAppsActionManager.isActionRegistered).isTrue() + verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any()) } @Test @@ -66,6 +110,7 @@ class AllAppsActionManagerTest { allAppsActionManager.isTaskbarPresent = false TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister. assertThat(allAppsActionManager.isActionRegistered).isFalse() + verify(quickstepKeyGestureEventsManager).unregisterAllAppsKeyGestureEvent() } @Test @@ -76,6 +121,7 @@ class AllAppsActionManagerTest { TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister. assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() assertThat(allAppsActionManager.isActionRegistered).isFalse() + verify(quickstepKeyGestureEventsManager).unregisterAllAppsKeyGestureEvent() } @Test @@ -88,4 +134,61 @@ class AllAppsActionManagerTest { assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() assertThat(allAppsActionManager.isActionRegistered).isTrue() } + + @Test + fun taskbarPresent_userSetupIncomplete_actionUnregistered() { + settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 0 + allAppsActionManager.isTaskbarPresent = true + assertThat(allAppsActionManager.isActionRegistered).isFalse() + } + + @Test + fun taskbarPresent_setupUiVisible_actionUnregistered() { + allAppsActionManager.isSetupUiVisible = true + allAppsActionManager.isTaskbarPresent = true + assertThat(allAppsActionManager.isActionRegistered).isFalse() + } + + @Test + fun taskbarPresent_userSetupCompleted_actionRegistered() { + settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 0 + allAppsActionManager.isTaskbarPresent = true + + settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 1 + TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register. + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isTrue() + verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any()) + } + + @Test + fun taskbarPresent_setupUiDismissed_actionRegistered() { + allAppsActionManager.isSetupUiVisible = true + allAppsActionManager.isTaskbarPresent = true + + allAppsActionManager.isSetupUiVisible = false + TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to register. + assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue() + assertThat(allAppsActionManager.isActionRegistered).isTrue() + verify(quickstepKeyGestureEventsManager).registerAllAppsKeyGestureEvent(any()) + } + + @Test + fun onDestroy_shouldUnregisterAllAppsKeyGestureHandler() { + allAppsActionManager.onDestroy() + + verify(quickstepKeyGestureEventsManager).unregisterAllAppsKeyGestureEvent() + } +} + +@LauncherAppSingleton +@Component(modules = [AllModulesForTest::class]) +interface AllAppsActionManagerTestComponent : LauncherAppComponent { + + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder + + override fun build(): AllAppsActionManagerTestComponent + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt new file mode 100644 index 0000000000..e62455fd02 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** Test for [DesktopFullscreenDrawParams] class. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DesktopFullscreenDrawParamsTest() { + private val params = + DesktopFullscreenDrawParams(mock(), cornerRadiusProvider = { CORNER_RADIUS }) + + @Test + fun setMiddleProgress_invariantCornerRadiusForDesktop() { + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS) + + params.setProgress(fullscreenProgress = 0.67f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS) + + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS) + } + + companion object { + const val CORNER_RADIUS = 32f + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/DisplayModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/DisplayModelTest.kt new file mode 100644 index 0000000000..3aab528437 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/DisplayModelTest.kt @@ -0,0 +1,83 @@ +/* + * 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.quickstep + +import android.content.Context +import android.view.Display +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.app.displaylib.DisplaysWithDecorationsRepositoryCompat +import java.io.PrintWriter +import kotlinx.coroutines.test.StandardTestDispatcher +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DisplayModelTest { + private val context: Context = ApplicationProvider.getApplicationContext() + private val systemDecorationChangeObserver = + SystemDecorationChangeObserver.INSTANCE.get(context) + private val displayRepositoryCompat = mock() + private val dispatcher = StandardTestDispatcher() + + class TestableResource : DisplayModel.DisplayResource() { + var isCleanupCalled = false + + override fun cleanup() { + isCleanupCalled = true + } + + override fun dump(prefix: String, writer: PrintWriter) { + // No-Op + } + } + + private val testableDisplayModel = + object : + DisplayModel( + context, + systemDecorationChangeObserver, + displayRepositoryCompat, + dispatcher, + ) { + override fun createDisplayResource(display: Display): TestableResource { + return TestableResource() + } + } + + @Test + fun testCreate() { + testableDisplayModel.storeDisplayResource(Display.DEFAULT_DISPLAY) + val resource = testableDisplayModel.getDisplayResource(Display.DEFAULT_DISPLAY) + assertNotNull(resource) + } + + @Test + fun testCleanAndDelete() { + testableDisplayModel.storeDisplayResource(Display.DEFAULT_DISPLAY) + val resource = testableDisplayModel.getDisplayResource(Display.DEFAULT_DISPLAY)!! + assertNotNull(resource) + testableDisplayModel.deleteDisplayResource(Display.DEFAULT_DISPLAY) + assert(resource.isCleanupCalled) + assertNull(testableDisplayModel.getDisplayResource(Display.DEFAULT_DISPLAY)) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java new file mode 100644 index 0000000000..109a52e2df --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java @@ -0,0 +1,74 @@ +/* + * 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.quickstep; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.util.LauncherMultivalentJUnit; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsState; + +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +public class FallbackSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase< + RecentsState, + RecentsActivity, + FallbackRecentsView, + FallbackSwipeHandler, + FallbackActivityInterface> { + + @Mock private RecentsActivity mRecentsActivity; + @Mock private FallbackRecentsView mRecentsView; + + + @Override + protected FallbackSwipeHandler createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture) { + return new FallbackSwipeHandler( + mContext, + mTaskAnimationManager, + mDeviceState, + mRotationTouchHelper, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController, + mMSDLPlayerWrapper); + } + + @NonNull + @Override + protected RecentsActivity getRecentsContainer() { + return mRecentsActivity; + } + + @NonNull + @Override + protected FallbackRecentsView getRecentsView() { + return mRecentsView; + } + + @NonNull + @Override + protected RecentsState getBaseState() { + return RecentsState.BG_LAUNCHER; + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt index 5d62a4c749..99b81e05eb 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt @@ -20,21 +20,17 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.launcher3.FakeInvariantDeviceProfileTest import com.android.quickstep.util.TaskCornerRadius -import com.android.quickstep.views.TaskView.FullscreenDrawParams import com.android.systemui.shared.system.QuickStepContract import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy +import org.mockito.kotlin.mock -/** Test for FullscreenDrawParams class. */ +/** Test for [FullscreenDrawParams] class. */ @SmallTest @RunWith(AndroidJUnit4::class) class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { - private lateinit var params: FullscreenDrawParams @Before @@ -46,115 +42,108 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { fun setStartProgress_correctCornerRadiusForTablet() { initializeVarsForTablet() - params.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 0f, parentScale = 1.0f, taskViewScale = 1.0f) val expectedRadius = TaskCornerRadius.get(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setFullProgress_correctCornerRadiusForTablet() { initializeVarsForTablet() - params.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f) val expectedRadius = QuickStepContract.getWindowCornerRadius(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setStartProgress_correctCornerRadiusForPhone() { initializeVarsForPhone() - params.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) val expectedRadius = TaskCornerRadius.get(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setFullProgress_correctCornerRadiusForPhone() { initializeVarsForPhone() - params.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) + params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f) val expectedRadius = QuickStepContract.getWindowCornerRadius(context) - assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentCornerRadius).isEqualTo(expectedRadius) } @Test fun setStartProgress_correctCornerRadiusForMultiDisplay() { - val display1Context = context - val display2Context = mock(Context::class.java) - val spyParams = spy(params) + val display1Context = mock() + val display2Context = mock() + val display1TaskRadius = TASK_CORNER_RADIUS + 1 + val display2TaskRadius = TASK_CORNER_RADIUS + 2 - val display1TaskRadius = TaskCornerRadius.get(display1Context) - val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context) - val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different. - val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different. - doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context) - doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context) + val params = + FullscreenDrawParams( + context, + taskCornerRadiusProvider = { context -> + when (context) { + display1Context -> display1TaskRadius + display2Context -> display2TaskRadius + else -> TASK_CORNER_RADIUS + } + }, + windowCornerRadiusProvider = { 0f }, + ) - spyParams.updateCornerRadius(display1Context) - spyParams.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1TaskRadius) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(TASK_CORNER_RADIUS) - spyParams.updateCornerRadius(display2Context) - spyParams.setProgress( - /* fullscreenProgress= */ 0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2TaskRadius) + params.updateCornerRadius(display1Context) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display1TaskRadius) + + params.updateCornerRadius(display2Context) + params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display2TaskRadius) } @Test fun setFullProgress_correctCornerRadiusForMultiDisplay() { - val display1Context = context - val display2Context = mock(Context::class.java) - val spyParams = spy(params) + val display1Context = mock() + val display2Context = mock() + val display1WindowRadius = WINDOW_CORNER_RADIUS + 1 + val display2WindowRadius = WINDOW_CORNER_RADIUS + 2 - val display1TaskRadius = TaskCornerRadius.get(display1Context) - val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context) - val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different. - val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different. - doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context) - doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context) + val params = + FullscreenDrawParams( + context, + taskCornerRadiusProvider = { 0f }, + windowCornerRadiusProvider = { context -> + when (context) { + display1Context -> display1WindowRadius + display2Context -> display2WindowRadius + else -> WINDOW_CORNER_RADIUS + } + }, + ) - spyParams.updateCornerRadius(display1Context) - spyParams.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1WindowRadius) + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(WINDOW_CORNER_RADIUS) - spyParams.updateCornerRadius(display2Context) - spyParams.setProgress( - /* fullscreenProgress= */ 1.0f, - /* parentScale= */ 1.0f, - /* taskViewScale= */ 1.0f, - ) - assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2WindowRadius) + params.updateCornerRadius(display1Context) + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display1WindowRadius) + + params.updateCornerRadius(display2Context) + params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f) + assertThat(params.currentCornerRadius).isEqualTo(display2WindowRadius) + } + + companion object { + const val TASK_CORNER_RADIUS = 56f + const val WINDOW_CORNER_RADIUS = 32f } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/HotseatWidthCalculationTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/HotseatWidthCalculationTest.kt index a38851098e..a2435188a1 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/HotseatWidthCalculationTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/HotseatWidthCalculationTest.kt @@ -39,7 +39,7 @@ class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { val dp = newDP() dp.isTaskbarPresentInApps = true - assertThat(dp.hotseatBarEndOffset).isEqualTo(510) + assertThat(dp.hotseatProfile.barEndOffset).isEqualTo(510) assertThat(dp.numShownHotseatIcons).isEqualTo(6) assertThat(dp.hotseatBorderSpace).isEqualTo(70) assertThat(dp.hotseatColumnSpan).isEqualTo(6) @@ -63,7 +63,7 @@ class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { val dp = newDP() dp.isTaskbarPresentInApps = true - assertThat(dp.hotseatBarEndOffset).isEqualTo(510) + assertThat(dp.hotseatProfile.barEndOffset).isEqualTo(510) assertThat(dp.numShownHotseatIcons).isEqualTo(4) assertThat(dp.hotseatBorderSpace).isEqualTo(40) assertThat(dp.hotseatColumnSpan).isEqualTo(6) @@ -86,7 +86,7 @@ class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { val dp = newDP() dp.isTaskbarPresentInApps = true - assertThat(dp.hotseatBarEndOffset).isEqualTo(705) + assertThat(dp.hotseatProfile.barEndOffset).isEqualTo(705) assertThat(dp.numShownHotseatIcons).isEqualTo(6) assertThat(dp.hotseatBorderSpace).isEqualTo(54) assertThat(dp.hotseatColumnSpan).isEqualTo(6) @@ -110,7 +110,7 @@ class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { val dp = newDP() dp.isTaskbarPresentInApps = true - assertThat(dp.hotseatBarEndOffset).isEqualTo(660) + assertThat(dp.hotseatProfile.barEndOffset).isEqualTo(660) assertThat(dp.numShownHotseatIcons).isEqualTo(6) assertThat(dp.hotseatBorderSpace).isEqualTo(100) assertThat(dp.hotseatColumnSpan).isEqualTo(6) @@ -131,7 +131,7 @@ class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { val dp = newDP() dp.isTaskbarPresentInApps = true - assertThat(dp.hotseatBarEndOffset).isEqualTo(660) + assertThat(dp.hotseatProfile.barEndOffset).isEqualTo(660) assertThat(dp.numShownHotseatIcons).isEqualTo(6) assertThat(dp.hotseatBorderSpace).isEqualTo(34) assertThat(dp.hotseatColumnSpan).isEqualTo(4) @@ -155,7 +155,7 @@ class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { val dp = newDP() dp.isTaskbarPresentInApps = true - assertThat(dp.hotseatBarEndOffset).isEqualTo(660) + assertThat(dp.hotseatProfile.barEndOffset).isEqualTo(660) assertThat(dp.numShownHotseatIcons).isEqualTo(5) assertThat(dp.hotseatBorderSpace).isEqualTo(36) assertThat(dp.hotseatColumnSpan).isEqualTo(4) @@ -174,7 +174,7 @@ class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() { val dp = newDP() dp.isTaskbarPresentInApps = true - assertThat(dp.hotseatBarEndOffset).isEqualTo(600) + assertThat(dp.hotseatProfile.barEndOffset).isEqualTo(600) assertThat(dp.numShownHotseatIcons).isEqualTo(6) assertThat(dp.hotseatBorderSpace).isEqualTo(48) assertThat(dp.hotseatColumnSpan).isEqualTo(8) diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt new file mode 100644 index 0000000000..d4d76c1e76 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt @@ -0,0 +1,130 @@ +package com.android.quickstep + +/* + * 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. + */ + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Flags +import com.android.launcher3.LauncherSettings.Favorites +import com.android.launcher3.util.SandboxApplication +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED) +class LauncherRestoreEventLoggerImplTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + @get:Rule val mSandboxContext = SandboxApplication() + + private lateinit var loggerUnderTest: LauncherRestoreEventLoggerImpl + + @Before + fun setup() { + loggerUnderTest = LauncherRestoreEventLoggerImpl(mSandboxContext) + } + + @Test + fun `logLauncherItemsRestoreFailed logs multiple items as failing restore`() { + // Given + val expectedDataType = "application" + val expectedError = "test_failure" + // When + loggerUnderTest.logLauncherItemsRestoreFailed( + dataType = expectedDataType, + count = 5, + error = expectedError, + ) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(0) + assertThat(actualResult.failCount).isEqualTo(5) + assertThat(actualResult.errors.keys).containsExactly(expectedError) + } + + @Test + fun `logLauncherItemsRestored logs multiple items as restored`() { + // Given + val expectedDataType = "application" + // When + loggerUnderTest.logLauncherItemsRestored(dataType = expectedDataType, count = 5) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(5) + assertThat(actualResult.failCount).isEqualTo(0) + assertThat(actualResult.errors.keys).isEmpty() + } + + @Test + fun `logSingleFavoritesItemRestored logs a single Favorites Item as restored`() { + // Given + val expectedDataType = "widget" + // When + loggerUnderTest.logSingleFavoritesItemRestored(favoritesId = Favorites.ITEM_TYPE_APPWIDGET) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(1) + assertThat(actualResult.failCount).isEqualTo(0) + assertThat(actualResult.errors.keys).isEmpty() + } + + @Test + fun `logSingleFavoritesItemRestoreFailed logs a single Favorites Item as failing restore`() { + // Given + val expectedDataType = "widget" + val expectedError = "test_failure" + // When + loggerUnderTest.logSingleFavoritesItemRestoreFailed( + favoritesId = Favorites.ITEM_TYPE_APPWIDGET, + error = expectedError, + ) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(0) + assertThat(actualResult.failCount).isEqualTo(1) + assertThat(actualResult.errors.keys).containsExactly(expectedError) + } + + @Test + fun `logFavoritesItemsRestoreFailed logs multiple Favorites Items as failing restore`() { + // Given + val expectedDataType = "deep_shortcut" + val expectedError = "test_failure" + // When + loggerUnderTest.logFavoritesItemsRestoreFailed( + favoritesId = Favorites.ITEM_TYPE_DEEP_SHORTCUT, + count = 5, + error = expectedError, + ) + // Then + val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first() + assertThat(actualResult.dataType).isEqualTo(expectedDataType) + assertThat(actualResult.successCount).isEqualTo(0) + assertThat(actualResult.failCount).isEqualTo(5) + assertThat(actualResult.errors.keys).containsExactly(expectedError) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt new file mode 100644 index 0000000000..8a7361f325 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt @@ -0,0 +1,148 @@ +/* + * 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. + */ + +package com.android.quickstep + +import android.graphics.PointF +import android.hardware.display.DisplayManager +import android.hardware.display.DisplayManagerGlobal +import android.view.Display +import android.view.Display.DEFAULT_DISPLAY +import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS +import android.view.DisplayInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.R +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppModule +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.MSDLPlayerWrapper +import com.android.launcher3.util.SandboxApplication +import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.shared.system.InputConsumerController +import dagger.BindsInstance +import dagger.Component +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Answers.RETURNS_DEEP_STUBS +import org.mockito.Mock +import org.mockito.Mockito.spy +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LauncherSwipeHandlerV2Test { + + @Mock private lateinit var taskAnimationManager: TaskAnimationManager + + @Mock private lateinit var deviceState: RecentsAnimationDeviceState + + private lateinit var gestureState: GestureState + @Mock private lateinit var inputConsumerController: InputConsumerController + + @Mock(answer = RETURNS_DEEP_STUBS) private lateinit var systemUiProxy: SystemUiProxy + + @Mock(answer = RETURNS_DEEP_STUBS) private lateinit var recentsModel: RecentsModel + + @Mock private lateinit var msdlPlayerWrapper: MSDLPlayerWrapper + + @Mock private lateinit var rotationTouchHelper: RotationTouchHelper + + private lateinit var underTest: LauncherSwipeHandlerV2 + + @get:Rule val mockitoRule = MockitoJUnit.rule() + @get:Rule val sandboxContext = SandboxApplication() + + private val flingSpeed = + -(sandboxContext.resources.getDimension(R.dimen.quickstep_fling_threshold_speed) + 1) + + private lateinit var displayManager: DisplayManager + + @Before + fun setup() { + val display = + Display( + DisplayManagerGlobal.getInstance(), + DEFAULT_DISPLAY, + DisplayInfo(), + DEFAULT_DISPLAY_ADJUSTMENTS, + ) + displayManager = sandboxContext.spyService(DisplayManager::class.java) + whenever(displayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(display) + whenever(displayManager.displays).thenReturn(arrayOf(display)) + + sandboxContext.initDaggerComponent( + DaggerTestComponent.builder() + .bindSystemUiProxy(systemUiProxy) + .bindRecentsModel(recentsModel) + ) + gestureState = + spy( + GestureState( + OverviewComponentObserver.INSTANCE.get(sandboxContext), + DEFAULT_DISPLAY, + 0, + ) + ) + + underTest = + LauncherSwipeHandlerV2( + sandboxContext, + taskAnimationManager, + deviceState, + rotationTouchHelper, + gestureState, + 0, + false, + inputConsumerController, + msdlPlayerWrapper, + ) + underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false) + } + + @Test + fun goHomeFromAppByTrackpad_updateEduStats() { + gestureState.setTrackpadGestureType(GestureState.TrackpadGestureType.THREE_FINGER) + underTest.onGestureEnded(flingSpeed, PointF(), /* horizontalTouchSlopPassed= */ false) + verify(systemUiProxy) + .updateContextualEduStats(/* isTrackpadGesture= */ eq(true), eq(GestureType.HOME)) + } + + @Test + fun goHomeFromAppByTouch_updateEduStats() { + underTest.onGestureEnded(flingSpeed, PointF(), /* horizontalTouchSlopPassed= */ false) + verify(systemUiProxy) + .updateContextualEduStats(/* isTrackpadGesture= */ eq(false), eq(GestureType.HOME)) + } +} + +@LauncherAppSingleton +@Component(modules = [LauncherAppModule::class]) +interface TestComponent : LauncherAppComponent { + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindSystemUiProxy(systemUiProxy: SystemUiProxy): Builder + + @BindsInstance fun bindRecentsModel(recentsModel: RecentsModel): Builder + + override fun build(): TestComponent + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java new file mode 100644 index 0000000000..0051a370dc --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java @@ -0,0 +1,105 @@ +/* + * 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.quickstep; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.Hotseat; +import com.android.launcher3.LauncherState; +import com.android.launcher3.Workspace; +import com.android.launcher3.WorkspaceStateTransitionAnimation; +import com.android.launcher3.statemanager.StateManager; +import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.LauncherMultivalentJUnit; +import com.android.quickstep.views.RecentsView; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +@Ignore +public class LauncherSwipeHandlerV2TestCase extends AbsSwipeUpHandlerTestCase< + LauncherState, + QuickstepLauncher, + RecentsView, + LauncherSwipeHandlerV2, + LauncherActivityInterface> { + + @Mock private QuickstepLauncher mQuickstepLauncher; + @Mock private RecentsView mRecentsView; + @Mock private Workspace mWorkspace; + @Mock private Hotseat mHotseat; + @Mock private WorkspaceStateTransitionAnimation mTransitionAnimation; + + @Before + public void setUpQuickStepLauncher() { + when(mQuickstepLauncher.createAtomicAnimationFactory()) + .thenReturn(new AtomicAnimationFactory<>(0)); + when(mQuickstepLauncher.getHotseat()).thenReturn(mHotseat); + doReturn(mWorkspace).when(mQuickstepLauncher).getWorkspace(); + doReturn(new StateManager(mQuickstepLauncher, LauncherState.NORMAL)) + .when(mQuickstepLauncher).getStateManager(); + + } + + @Before + public void setUpWorkspace() { + when(mWorkspace.getStateTransitionAnimation()).thenReturn(mTransitionAnimation); + } + + @NonNull + @Override + protected LauncherSwipeHandlerV2 createSwipeHandler( + long touchTimeMs, boolean continuingLastGesture) { + return new LauncherSwipeHandlerV2( + mContext, + mTaskAnimationManager, + mDeviceState, + mRotationTouchHelper, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController, + mMSDLPlayerWrapper); + } + + @NonNull + @Override + protected QuickstepLauncher getRecentsContainer() { + return mQuickstepLauncher; + } + + @NonNull + @Override + protected RecentsView getRecentsView() { + return mRecentsView; + } + + @NonNull + @Override + protected LauncherState getBaseState() { + return LauncherState.NORMAL; + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java new file mode 100644 index 0000000000..0ff142a47b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/MultiStateCallbackTest.java @@ -0,0 +1,271 @@ +/* + * 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.quickstep; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.util.LauncherMultivalentJUnit; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.Consumer; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +public class MultiStateCallbackTest { + + private int mFlagCount = 0; + private int getNextStateFlag() { + int index = 1 << mFlagCount; + mFlagCount++; + return index; + } + + private final MultiStateCallback mMultiStateCallback = new MultiStateCallback(new String[0]); + private final Runnable mCallback = spy(new Runnable() { + @Override + public void run() {} + }); + private final Consumer mListener = spy(new Consumer() { + @Override + public void accept(Boolean isOn) {} + }); + + @Test + public void testSetState_trackedProperly() { + int watchedAnime = getNextStateFlag(); + + assertThat(mMultiStateCallback.getState()).isEqualTo(0); + assertThat(mMultiStateCallback.hasStates(watchedAnime)).isFalse(); + + mMultiStateCallback.setState(watchedAnime); + + assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime); + assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue(); + } + + @Test + public void testSetState_withMultipleStates_trackedProperly() { + int watchedAnime = getNextStateFlag(); + int sharedMemes = getNextStateFlag(); + + mMultiStateCallback.setState(watchedAnime); + mMultiStateCallback.setState(sharedMemes); + + assertThat(mMultiStateCallback.getState()).isEqualTo(watchedAnime | sharedMemes); + assertThat(mMultiStateCallback.hasStates(watchedAnime)).isTrue(); + assertThat(mMultiStateCallback.hasStates(sharedMemes)).isTrue(); + assertThat(mMultiStateCallback.hasStates(watchedAnime | sharedMemes)).isTrue(); + } + + @Test + public void testClearState_trackedProperly() { + int lovedAnime = getNextStateFlag(); + + mMultiStateCallback.setState(lovedAnime); + mMultiStateCallback.clearState(lovedAnime); + + assertThat(mMultiStateCallback.getState()).isEqualTo(0); + assertThat(mMultiStateCallback.hasStates(lovedAnime)).isFalse(); + } + + @Test + public void testClearState_withMultipleState_trackedProperly() { + int lovedAnime = getNextStateFlag(); + int talkedAboutAnime = getNextStateFlag(); + + mMultiStateCallback.setState(lovedAnime); + mMultiStateCallback.setState(talkedAboutAnime); + mMultiStateCallback.clearState(talkedAboutAnime); + + assertThat(mMultiStateCallback.getState()).isEqualTo(lovedAnime); + assertThat(mMultiStateCallback.hasStates(lovedAnime)).isTrue(); + assertThat(mMultiStateCallback.hasStates(talkedAboutAnime)).isFalse(); + assertThat(mMultiStateCallback.hasStates(lovedAnime | talkedAboutAnime)).isFalse(); + } + + @Test + public void testCallbackDoesNotRun_withoutState() { + int watchedOnePiece = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedOnePiece, mCallback); + + verify(mCallback, never()).run(); + } + + @Test + public void testCallbackDoesNotRun_whenNotTracked() { + int watchedJujutsuKaisen = getNextStateFlag(); + + mMultiStateCallback.setState(watchedJujutsuKaisen); + + verify(mCallback, never()).run(); + } + + @Test + public void testCallbackRuns_afterTrackedAndStateSet() { + int watchedHunterXHunter = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback); + mMultiStateCallback.setState(watchedHunterXHunter); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_onUiThread() { + int watchedHunterXHunter = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedHunterXHunter, mCallback); + mMultiStateCallback.setStateOnUiThread(watchedHunterXHunter); + + runOnMainSync(() -> verify(mCallback, times(1)).run()); + } + + @Test + public void testCallbackRuns_agnosticallyToCallOrder() { + int watchedFullMetalAlchemist = getNextStateFlag(); + + mMultiStateCallback.setState(watchedFullMetalAlchemist); + mMultiStateCallback.runOnceAtState(watchedFullMetalAlchemist, mCallback); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_onlyOnceAfterStateSet() { + int watchedBleach = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedBleach, mCallback); + mMultiStateCallback.setState(watchedBleach); + mMultiStateCallback.setState(watchedBleach); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_onlyOnceAfterClearState() { + int rememberedGreatShow = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(rememberedGreatShow, mCallback); + mMultiStateCallback.setState(rememberedGreatShow); + mMultiStateCallback.clearState(rememberedGreatShow); + mMultiStateCallback.setState(rememberedGreatShow); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackDoesNotRun_withoutFullStateSet() { + int watchedMobPsycho = getNextStateFlag(); + int watchedVinlandSaga = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedMobPsycho | watchedVinlandSaga, mCallback); + mMultiStateCallback.setState(watchedMobPsycho); + + verify(mCallback, times(0)).run(); + } + + @Test + public void testCallbackRuns_withFullStateSet_agnosticallyToCallOrder() { + int watchedReZero = getNextStateFlag(); + int watchedJojosBizareAdventure = getNextStateFlag(); + + mMultiStateCallback.setState(watchedJojosBizareAdventure); + mMultiStateCallback.runOnceAtState(watchedReZero | watchedJojosBizareAdventure, mCallback); + mMultiStateCallback.setState(watchedReZero); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackRuns_withFullStateSet_asIntegerMask() { + int watchedPokemon = getNextStateFlag(); + int watchedDigimon = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedPokemon | watchedDigimon, mCallback); + mMultiStateCallback.setState(watchedPokemon | watchedDigimon); + + verify(mCallback, times(1)).run(); + } + + @Test + public void testCallbackDoesNotRun_afterClearState() { + int watchedMonster = getNextStateFlag(); + int watchedPingPong = getNextStateFlag(); + + mMultiStateCallback.runOnceAtState(watchedMonster | watchedPingPong, mCallback); + mMultiStateCallback.setState(watchedMonster); + mMultiStateCallback.clearState(watchedMonster); + mMultiStateCallback.setState(watchedPingPong); + + verify(mCallback, times(0)).run(); + } + + @Test + public void testlistenerRuns_multipleTimes() { + int watchedSteinsGate = getNextStateFlag(); + + mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener); + mMultiStateCallback.setState(watchedSteinsGate); + + // Called exactly one + verify(mListener, times(1)).accept(anyBoolean()); + // Called exactly once with isOn = true + verify(mListener, times(1)).accept(eq(true)); + // Never called with isOn = false + verify(mListener, times(0)).accept(eq(false)); + + mMultiStateCallback.clearState(watchedSteinsGate); + + // Called exactly twice + verify(mListener, times(2)).accept(anyBoolean()); + // Called exactly once with isOn = true + verify(mListener, times(1)).accept(eq(true)); + // Called exactly once with isOn = false + verify(mListener, times(1)).accept(eq(false)); + } + + @Test + public void testlistenerDoesNotRun_forUnchangedState() { + int watchedSteinsGate = getNextStateFlag(); + + mMultiStateCallback.addChangeListener(watchedSteinsGate, mListener); + mMultiStateCallback.setState(watchedSteinsGate); + mMultiStateCallback.setState(watchedSteinsGate); + + // State remained unchanged + verify(mListener, times(1)).accept(anyBoolean()); + // Called exactly once with isOn = true + verify(mListener, times(1)).accept(eq(true)); + } + + private static void runOnMainSync(Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt new file mode 100644 index 0000000000..d98cb989b5 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt @@ -0,0 +1,433 @@ +/* + * 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.quickstep + +import android.content.Intent +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.filters.SmallTest +import com.android.app.displaylib.DisplayRepository +import com.android.app.displaylib.fakes.FakePerDisplayRepository +import com.android.app.displaylib.PerDisplayRepository +import com.android.launcher3.LauncherState +import com.android.launcher3.statemanager.StateManager +import com.android.launcher3.statemanager.StatefulActivity +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.RunnableList +import com.android.launcher3.util.TestDispatcherProvider +import com.android.quickstep.OverviewCommandHelper.CommandInfo +import com.android.quickstep.OverviewCommandHelper.CommandInfo.CommandStatus +import com.android.quickstep.OverviewCommandHelper.CommandType +import com.android.quickstep.OverviewCommandHelper.Companion.TOGGLE_PREVIOUS_TIMEOUT_MS +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.TaskView +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.spy +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(LauncherMultivalentJUnit::class) +@OptIn(ExperimentalCoroutinesApi::class) +class OverviewCommandHelperTest { + private lateinit var sut: OverviewCommandHelper + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private var pendingCallbacksWithDelays = mutableListOf() + + private val displayRepository: DisplayRepository = mock() + private val executeCommandDisplayIds = mutableListOf() + + private val recentView: RecentsView<*, *> = mock() + private val stateManager: StateManager> = mock() + private val containerInterface: BaseActivityInterface = mock() + private val taskAnimationManager: TaskAnimationManager = mock() + private val touchInteractionService: TouchInteractionService = mock() + private var elapsedRealtime = 100L + + private fun setupDefaultDisplay() { + whenever(displayRepository.displayIds).thenReturn(MutableStateFlow(setOf(DEFAULT_DISPLAY))) + } + + private fun setupMultipleDisplays() { + whenever(displayRepository.displayIds) + .thenReturn(MutableStateFlow(setOf(DEFAULT_DISPLAY, 1))) + } + + @Suppress("UNCHECKED_CAST") + @Before + fun setup() { + setupDefaultDisplay() + + val overviewComponentObserver = mock() + whenever(overviewComponentObserver.getContainerInterface(any())) + .thenReturn(containerInterface) + whenever(overviewComponentObserver.getHomeIntent(any())).thenReturn(mock()) + whenever(recentView.getStateManager()).thenReturn(stateManager) + whenever(containerInterface.switchToRecentsIfVisible(any())).thenReturn(true) + whenever(taskAnimationManager.maybeStartHomeAction(any())).thenAnswer { invocation -> + invocation.getArgument(0).run() + } + + sut = + spy( + OverviewCommandHelper( + touchInteractionService = touchInteractionService, + overviewComponentObserver = overviewComponentObserver, + dispatcherProvider = TestDispatcherProvider(dispatcher), + displayRepository = displayRepository, + taskbarManager = mock(), + taskAnimationManagerRepository = + FakePerDisplayRepository { _ -> taskAnimationManager }, + elapsedRealtime = ::elapsedRealtime, + ) + ) + } + + private fun addCallbackDelay(delayInMillis: Long = 0) { + pendingCallbacksWithDelays.add(delayInMillis) + } + + private fun mockExecuteCommand() { + doAnswer { invocation -> + val pendingCallback = invocation.arguments[1] as () -> Unit + + val delayInMillis = pendingCallbacksWithDelays.removeFirstOrNull() + if (delayInMillis != null) { + runBlocking { + testScope.backgroundScope.launch { + delay(delayInMillis) + pendingCallback.invoke() + } + } + } + val commandInfo = invocation.arguments[0] as CommandInfo + executeCommandDisplayIds.add(commandInfo.displayId) + delayInMillis == null // if no callback to execute, returns success + } + .`when`(sut) + .executeCommand(any(), any()) + } + + @Test + fun whenFirstCommandIsAdded_executeCommandImmediately() = + testScope.runTest { + mockExecuteCommand() + // Add command to queue + val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenFirstCommandIsAdded_executeCommandImmediately_WithCallbackDelay() = + testScope.runTest { + mockExecuteCommand() + addCallbackDelay(100) + + // Add command to queue + val commandType = CommandType.HOME + val commandInfo: CommandInfo = sut.addCommand(commandType)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.PROCESSING) + + advanceTimeBy(200L) + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenFirstCommandIsPendingCallback_NextCommandWillWait() = + testScope.runTest { + mockExecuteCommand() + // Add command to queue + addCallbackDelay(100) + val commandType1 = CommandType.HOME + val commandInfo1: CommandInfo = sut.addCommand(commandType1)!! + assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE) + + addCallbackDelay(100) + val commandType2 = CommandType.SHOW_ALT_TAB + val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + runCurrent() + assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + advanceTimeBy(101L) + assertThat(commandInfo1.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) + + advanceTimeBy(101L) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenCommandTakesTooLong_TriggerTimeout_AndExecuteNextCommand() = + testScope.runTest { + mockExecuteCommand() + // Add command to queue + addCallbackDelay(QUEUE_TIMEOUT) + val commandType1 = CommandType.HOME + val commandInfo1: CommandInfo = sut.addCommand(commandType1)!! + assertThat(commandInfo1.status).isEqualTo(CommandStatus.IDLE) + + addCallbackDelay(100) + val commandType2 = CommandType.SHOW_ALT_TAB + val commandInfo2: CommandInfo = sut.addCommand(commandType2)!! + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + runCurrent() + assertThat(commandInfo1.status).isEqualTo(CommandStatus.PROCESSING) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.IDLE) + + advanceTimeBy(QUEUE_TIMEOUT) + assertThat(commandInfo1.status).isEqualTo(CommandStatus.CANCELED) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.PROCESSING) + + advanceTimeBy(101) + assertThat(commandInfo2.status).isEqualTo(CommandStatus.COMPLETED) + } + + @Test + fun whenAllDisplaysCommandIsAdded_singleCommandProcessedForDefaultDisplay() = + testScope.runTest { + mockExecuteCommand() + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = sut.addCommandsForAllDisplays(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(DEFAULT_DISPLAY) + } + + @Test + fun whenAllDisplaysCommandIsAdded_multipleCommandsProcessedForMultipleDisplays() = + testScope.runTest { + mockExecuteCommand() + setupMultipleDisplays() + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = sut.addCommandsForAllDisplays(CommandType.HOME)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds) + .containsExactly(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID) + } + + @Test + fun whenAllExceptDisplayCommandIsAdded_otherDisplayProcessed() = + testScope.runTest { + mockExecuteCommand() + setupMultipleDisplays() + executeCommandDisplayIds.clear() + // Add command to queue + val commandInfo: CommandInfo = + sut.addCommandsForDisplaysExcept(CommandType.HOME, DEFAULT_DISPLAY)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(EXTERNAL_DISPLAY_ID) + } + + @Test + fun whenSingleDisplayCommandIsAdded_thatDisplayIsProcessed() = + testScope.runTest { + mockExecuteCommand() + executeCommandDisplayIds.clear() + val displayId = 5 + // Add command to queue + val commandInfo: CommandInfo = sut.addCommand(CommandType.HOME, displayId)!! + assertThat(commandInfo.status).isEqualTo(CommandStatus.IDLE) + runCurrent() + assertThat(commandInfo.status).isEqualTo(CommandStatus.COMPLETED) + assertThat(executeCommandDisplayIds).containsExactly(displayId) + } + + @Test + fun recentViewNotVisible_toggleOverviewPrev_goToOverview() = + testScope.runTest { + whenever(containerInterface.getVisibleRecentsView>()).thenReturn(null) + sut.addCommand(CommandType.TOGGLE_OVERVIEW_PREVIOUS)!! + runCurrent() + verify(containerInterface).switchToRecentsIfVisible(any()) + } + + @Test + fun recentViewVisible_toggleOverviewPrev_goToHome() = + testScope.runTest { + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + sut.addCommand(CommandType.TOGGLE_OVERVIEW_PREVIOUS)!! + runCurrent() + verify(recentView).startHome() + } + + @Test + fun recentViewVisible_hasRunningTask_toggleOverviewPrev_goToPrevTask() = + testScope.runTest { + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + val mockTask = mock() + whenever(recentView.runningTaskView).thenReturn(mockTask) + + sut.addCommand(CommandType.TOGGLE_OVERVIEW_PREVIOUS)!! + runCurrent() + + verify(mockTask).launchWithAnimation() + } + + @Test + fun recentViewVisible_hasRunningTask_toggle() = + testScope.runTest { + val callbackList = RunnableList() + + fun getMockTask(vararg elements: Int) = + mock().apply { + whenever(taskIdSet).thenReturn(elements.toSet()) + whenever(recentView.getTaskViewByTaskIds(elements)).thenReturn(this) + whenever(launchWithAnimation()).thenReturn(callbackList) + } + val mockTask1 = getMockTask(1, 2, 3) + val mockTask2 = getMockTask(4, 5) + val mockTask3 = getMockTask(6, 7) + + // TOGGLE with a runningTaskView should go to nextTaskView + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + whenever(recentView.runningTaskView).thenReturn(mockTask1) + whenever(recentView.nextTaskView).thenReturn(mockTask2) + sut.addCommand(CommandType.TOGGLE) + runCurrent() + verify(mockTask2).launchWithAnimation() + callbackList.executeAllAndDestroy() + + // Next TOGGLE with runningTaskView will return to previous runningTaskView + whenever(recentView.runningTaskView).thenReturn(mockTask2) + whenever(recentView.nextTaskView).thenReturn(mockTask3) + sut.addCommand(CommandType.TOGGLE) + runCurrent() + verify(mockTask1).launchWithAnimation() + callbackList.executeAllAndDestroy() + + // After TOGGLE_PREVIOUS_TIMEOUT_MS has passed, subsequent TOGGLE will go to + // nextTaskView again. + whenever(recentView.runningTaskView).thenReturn(mockTask1) + whenever(recentView.nextTaskView).thenReturn(mockTask3) + sut.addCommand(CommandType.TOGGLE) + elapsedRealtime += TOGGLE_PREVIOUS_TIMEOUT_MS + runCurrent() + verify(mockTask3).launchWithAnimation() + } + + @Test + fun recentViewVisible_noRunningTask_toggle_goToFirstNonDesktopTaskView() = + testScope.runTest { + val firstNonDesktopTaskView = mock() + val lastDesktopTaskView = mock() + val previousTaskView = mock() + val nextTaskView = mock() + + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + whenever(recentView.runningTaskView).thenReturn(null) + whenever(recentView.firstNonDesktopTaskView).thenReturn(firstNonDesktopTaskView) + whenever(recentView.lastDesktopTaskView).thenReturn(lastDesktopTaskView) + whenever(recentView.previousTaskView).thenReturn(previousTaskView) + whenever(recentView.nextTaskView).thenReturn(nextTaskView) + sut.addCommand(CommandType.TOGGLE) + runCurrent() + verify(firstNonDesktopTaskView).launchWithAnimation() + } + + @Test + fun recentViewVisible_noRunningTask_toggle_goToLastDesktopTaskView() = + testScope.runTest { + val firstNonDesktopTaskView = mock() + val lastDesktopTaskView = mock() + val firstTaskView = mock() + val previousTaskView = mock() + val nextTaskView = mock() + + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + whenever(recentView.runningTaskView).thenReturn(null) + whenever(recentView.firstNonDesktopTaskView).thenReturn(null) + whenever(recentView.lastDesktopTaskView).thenReturn(lastDesktopTaskView) + whenever(recentView.firstTaskView).thenReturn(firstTaskView) + whenever(recentView.previousTaskView).thenReturn(previousTaskView) + whenever(recentView.nextTaskView).thenReturn(nextTaskView) + sut.addCommand(CommandType.TOGGLE) + runCurrent() + verify(lastDesktopTaskView).launchWithAnimation() + } + + @Test + fun recentViewVisible_hasRunningTask_toggle_goToPreviousTaskView() = + testScope.runTest { + val runningTaskView = mock() + val firstTaskView = mock() + val previousTaskView = mock() + + whenever(containerInterface.getVisibleRecentsView>()) + .thenReturn(recentView) + whenever(recentView.runningTaskView).thenReturn(runningTaskView) + whenever(recentView.firstTaskView).thenReturn(firstTaskView) + whenever(recentView.previousTaskView).thenReturn(previousTaskView) + whenever(recentView.nextTaskView).thenReturn(null) + sut.addCommand(CommandType.TOGGLE) + runCurrent() + verify(previousTaskView).launchWithAnimation() + } + + @Test + fun whenHomeCommandIsAdded_executeHomeAction() = + testScope.runTest { + sut.addCommand(CommandType.HOME) + runCurrent() + verify(taskAnimationManager).maybeStartHomeAction(any()) + verify(touchInteractionService).startActivity(any()) + } + + private companion object { + const val QUEUE_TIMEOUT = 5001L + const val EXTERNAL_DISPLAY_ID = 1 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java new file mode 100644 index 0000000000..e8f67e7221 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.TestCase.assertNull; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManager.RecentTaskInfo; +import android.app.KeyguardManager; +import android.app.TaskInfo; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.Executors; +import com.android.launcher3.util.LooperExecutor; +import com.android.quickstep.util.DesktopTask; +import com.android.quickstep.util.GroupTask; +import com.android.quickstep.views.TaskViewType; +import com.android.systemui.shared.recents.model.Task; +import com.android.wm.shell.shared.GroupedTaskInfo; +import com.android.wm.shell.shared.split.SplitBounds; +import com.android.wm.shell.shared.split.SplitScreenConstants; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RecentTasksListTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private SystemUiProxy mSystemUiProxy; + @Mock + private TopTaskTracker mTopTaskTracker; + + // Class under test + private RecentTasksList mRecentTasksList; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + LooperExecutor mainThreadExecutor = Executors.MAIN_EXECUTOR; + KeyguardManager mockKeyguardManager = mock(KeyguardManager.class); + + // Set desktop mode supported + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true); + when(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops)) + .thenReturn(true); + + mRecentTasksList = new RecentTasksList(mContext, mainThreadExecutor, + mockKeyguardManager, mSystemUiProxy, mTopTaskTracker, + mock(DaggerSingletonTracker.class)); + } + + @Test + public void onRecentTasksChanged_doesNotFetchTasks() throws Exception { + mRecentTasksList.onRecentTasksChanged(); + verify(mSystemUiProxy, times(0)) + .getRecentTasks(anyInt(), anyInt()); + } + + @Test + public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception { + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks( + new RecentTaskInfo(), new RecentTaskInfo(), new SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ 1, + /* rightBottomTaskId = */ 2, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50)); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, + true); + + assertEquals(1, taskList.size()); + taskList.get(0).getTasks().forEach(t -> assertNull(t.taskDescription.getLabel())); + } + + @Test + public void loadTasksInBackground_GetRecentTasksException() throws Exception { + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenThrow(new SystemUiProxy.GetRecentTasksException("task load failed")); + + RecentTasksList.TaskLoadResult taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE, -1, false); + + assertThat(taskList.mRequestId).isEqualTo(-1); + assertThat(taskList).isEmpty(); + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void loadTasksInBackground_freeformTask_multiDesksInMultiDisplays() throws Exception { + List tasksInDefaultDesk1 = Arrays.asList( + createRecentTaskInfo(/* taskId = */ 1, DEFAULT_DISPLAY), + createRecentTaskInfo(/* taskId = */ 4, DEFAULT_DISPLAY)); + List tasksInDefaultDesk2 = Arrays.asList( + createRecentTaskInfo(/* taskId = */ 2, DEFAULT_DISPLAY), + createRecentTaskInfo(/* taskId = */ 3, DEFAULT_DISPLAY)); + List tasksInExtend = Arrays.asList( + createRecentTaskInfo(/* taskId = */ 5, /* displayId = */ 1), + createRecentTaskInfo(/* taskId = */ 6, /* displayId = */ 1)); + GroupedTaskInfo recentTaskInfosOfDesk1 = GroupedTaskInfo.forDeskTasks(/* deskId = */1, + DEFAULT_DISPLAY, tasksInDefaultDesk1, /* minimizedTaskIds = */ + Collections.emptySet()); + GroupedTaskInfo recentTaskInfosOfDesk2 = GroupedTaskInfo.forDeskTasks(/* deskId = */2, + DEFAULT_DISPLAY, tasksInDefaultDesk2, /* minimizedTaskIds = */ + Collections.emptySet()); + GroupedTaskInfo recentTaskInfosOfDesk3 = GroupedTaskInfo.forDeskTasks(/* deskId = */3, + /* displayId = */ 1, tasksInExtend, /* minimizedTaskIds = */ + Collections.emptySet()); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())).thenReturn( + new ArrayList<>(Arrays.asList(recentTaskInfosOfDesk1, recentTaskInfosOfDesk2, + recentTaskInfosOfDesk3))); + + List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, + false); + + assertThat(taskList).hasSize(3); + assertThat(taskList.get(2).taskViewType).isEqualTo(TaskViewType.DESKTOP); + List actualFreeformTasksInDesk1 = taskList.get(2).getTasks(); + assertThat(actualFreeformTasksInDesk1).hasSize(2); + assertThat(actualFreeformTasksInDesk1.get(0).key.id).isEqualTo(1); + assertThat(actualFreeformTasksInDesk1.get(0).isMinimized).isFalse(); + assertThat(actualFreeformTasksInDesk1.get(1).key.id).isEqualTo(4); + assertThat(actualFreeformTasksInDesk1.get(1).isMinimized).isFalse(); + assertThat(((DesktopTask) taskList.get(2)).getDeskId()).isEqualTo(1); + assertThat(((DesktopTask) taskList.get(2)).getDisplayId()).isEqualTo(DEFAULT_DISPLAY); + + assertThat(taskList.get(1).taskViewType).isEqualTo(TaskViewType.DESKTOP); + List actualFreeformTasksInDesk2 = taskList.get(1).getTasks(); + assertThat(actualFreeformTasksInDesk2).hasSize(2); + assertThat(actualFreeformTasksInDesk2.get(0).key.id).isEqualTo(2); + assertThat(actualFreeformTasksInDesk2.get(0).isMinimized).isFalse(); + assertThat(actualFreeformTasksInDesk2.get(1).key.id).isEqualTo(3); + assertThat(actualFreeformTasksInDesk2.get(1).isMinimized).isFalse(); + assertThat(((DesktopTask) taskList.get(1)).getDeskId()).isEqualTo(2); + assertThat(((DesktopTask) taskList.get(1)).getDisplayId()).isEqualTo(DEFAULT_DISPLAY); + + assertThat(taskList.get(0).taskViewType).isEqualTo(TaskViewType.DESKTOP); + List actualFreeformTasksInDesk3 = taskList.get(0).getTasks(); + assertThat(actualFreeformTasksInDesk3).hasSize(2); + assertThat(actualFreeformTasksInDesk3.get(0).key.id).isEqualTo(5); + assertThat(actualFreeformTasksInDesk3.get(0).isMinimized).isFalse(); + assertThat(actualFreeformTasksInDesk3.get(1).key.id).isEqualTo(6); + assertThat(actualFreeformTasksInDesk3.get(1).isMinimized).isFalse(); + assertThat(((DesktopTask) taskList.get(0)).getDeskId()).isEqualTo(3); + assertThat(((DesktopTask) taskList.get(0)).getDisplayId()).isEqualTo(1); + } + + @Test + public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() throws Exception { + String taskDescription = "Wheeee!"; + RecentTaskInfo task1 = new RecentTaskInfo(); + task1.taskDescription = new ActivityManager.TaskDescription(taskDescription); + RecentTaskInfo task2 = new RecentTaskInfo(); + task2.taskDescription = new ActivityManager.TaskDescription(); + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(task1, task2, + new SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ 1, + /* rightBottomTaskId = */ 2, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50)); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, + false); + + assertEquals(1, taskList.size()); + var tasks = taskList.get(0).getTasks(); + assertEquals(2, tasks.size()); + assertEquals(taskDescription, tasks.get(0).taskDescription.getLabel()); + assertNull(tasks.get(1).taskDescription.getLabel()); + } + + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + public void loadTasksInBackground_freeformTask_createsDesktopTaskPerDisplay() throws Exception { + List tasks = Arrays.asList( + createRecentTaskInfo(1 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(4 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(5 /* taskId */, 1 /* displayId */), + createRecentTaskInfo(6 /* taskId */, 1 /* displayId */)); + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks( + 0 /* deskId */, DEFAULT_DISPLAY, tasks, + Collections.emptySet() /* minimizedTaskIds */); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */); + + assertEquals(2, taskList.size()); + assertEquals(TaskViewType.DESKTOP, taskList.get(0).taskViewType); + List actualFreeformTasksDefaultDisplay = taskList.get(0).getTasks(); + assertEquals(2, actualFreeformTasksDefaultDisplay.size()); + assertEquals(1, actualFreeformTasksDefaultDisplay.get(0).key.id); + assertFalse(actualFreeformTasksDefaultDisplay.get(0).isMinimized); + assertEquals(4, actualFreeformTasksDefaultDisplay.get(1).key.id); + assertFalse(actualFreeformTasksDefaultDisplay.get(1).isMinimized); + + List actualFreeformTasksExternalDisplay = taskList.get(1).getTasks(); + assertEquals(2, actualFreeformTasksExternalDisplay.size()); + assertEquals(5, actualFreeformTasksExternalDisplay.get(0).key.id); + assertFalse(actualFreeformTasksExternalDisplay.get(0).isMinimized); + assertEquals(6, actualFreeformTasksExternalDisplay.get(1).key.id); + assertFalse(actualFreeformTasksExternalDisplay.get(1).isMinimized); + } + + @Test + public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_createDesktopTask() + throws Exception { + List tasks = Arrays.asList( + createRecentTaskInfo(1 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(4 /* taskId */, DEFAULT_DISPLAY), + createRecentTaskInfo(5 /* taskId */, DEFAULT_DISPLAY)); + Set minimizedTaskIds = + Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet()); + GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks( + 0 /* deskId */, DEFAULT_DISPLAY, tasks, minimizedTaskIds); + when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt())) + .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); + + List taskList = mRecentTasksList.loadTasksInBackground( + Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */); + + assertEquals(1, taskList.size()); + assertEquals(TaskViewType.DESKTOP, taskList.get(0).taskViewType); + List actualFreeformTasks = taskList.get(0).getTasks(); + assertEquals(3, actualFreeformTasks.size()); + assertEquals(1, actualFreeformTasks.get(0).key.id); + assertTrue(actualFreeformTasks.get(0).isMinimized); + assertEquals(4, actualFreeformTasks.get(1).key.id); + assertTrue(actualFreeformTasks.get(1).isMinimized); + assertEquals(5, actualFreeformTasks.get(2).key.id); + assertTrue(actualFreeformTasks.get(2).isMinimized); + } + + private TaskInfo createRecentTaskInfo(int taskId, int displayId) { + RecentTaskInfo recentTaskInfo = new RecentTaskInfo(); + recentTaskInfo.taskId = taskId; + recentTaskInfo.displayId = displayId; + return recentTaskInfo; + } +} diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt similarity index 59% rename from quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt index 80fbce7265..ba9e238c0d 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt @@ -1,17 +1,22 @@ package com.android.quickstep -import android.content.Context -import android.testing.AndroidTestingRunner -import androidx.test.core.app.ApplicationProvider +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.annotation.UiThreadTest import androidx.test.filters.SmallTest +import com.android.launcher3.dagger.LauncherComponentProvider import com.android.launcher3.util.DisplayController.CHANGE_DENSITY import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE import com.android.launcher3.util.DisplayController.CHANGE_ROTATION import com.android.launcher3.util.DisplayController.Info +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.launcher3.util.NavigationMode +import com.android.launcher3.util.SandboxApplication import com.android.quickstep.util.GestureExclusionManager import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP @@ -22,7 +27,9 @@ import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SE import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED import com.google.common.truth.Truth.assertThat +import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -30,24 +37,45 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import org.mockito.kotlin.doReturn -import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever /** Unit test for [RecentsAnimationDeviceState]. */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@UiThreadTest +@RunWith(LauncherMultivalentJUnit::class) class RecentsAnimationDeviceStateTest { + @get:Rule val context = SandboxApplication() + @Mock private lateinit var exclusionManager: GestureExclusionManager @Mock private lateinit var info: Info + @Mock private lateinit var rotationTouchHelper: RotationTouchHelper - private val context = ApplicationProvider.getApplicationContext() as Context private lateinit var underTest: RecentsAnimationDeviceState @Before fun setup() { MockitoAnnotations.initMocks(this) - underTest = RecentsAnimationDeviceState(context, exclusionManager) + + val component = LauncherComponentProvider.get(context) + underTest = + RecentsAnimationDeviceState( + context, + DEFAULT_DISPLAY, + rotationTouchHelper, + exclusionManager, + component.displayController, + component.contextualSearchStateManager, + component.settingsCache, + component.daggerSingletonTracker, + ) + } + + @After + fun tearDown() { + UI_HELPER_EXECUTOR.submit {}.get() + MAIN_EXECUTOR.submit {}.get() } @Test @@ -64,7 +92,7 @@ class RecentsAnimationDeviceStateTest { underTest.registerExclusionListener() - verifyZeroInteractions(exclusionManager) + verifyNoMoreInteractions(exclusionManager) } @Test @@ -85,7 +113,7 @@ class RecentsAnimationDeviceStateTest { underTest.unregisterExclusionListener() - verifyZeroInteractions(exclusionManager) + verifyNoMoreInteractions(exclusionManager) } @Test @@ -116,33 +144,33 @@ class RecentsAnimationDeviceStateTest { underTest.onDisplayInfoChanged(context, info, CHANGE_DENSITY) - verifyZeroInteractions(exclusionManager) + verifyNoMoreInteractions(exclusionManager) } @Test fun trackpadGesturesNotAllowedForSelectedStates() { - val disablingStates = GESTURE_DISABLING_SYSUI_STATES + - SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED + val disablingStates = + GESTURE_DISABLING_SYSUI_STATES + SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED allSysUiStates().forEach { state -> val canStartGesture = !disablingStates.contains(state) - underTest.setSystemUiFlags(state) + underTest.setSysUIStateFlags(state) assertThat(underTest.canStartTrackpadGesture()).isEqualTo(canStartGesture) } } @Test fun trackpadGesturesNotAllowedIfHomeAndOverviewIsDisabled() { - val stateToExpectedResult = mapOf( - SYSUI_STATE_HOME_DISABLED to true, - SYSUI_STATE_OVERVIEW_DISABLED to true, - DEFAULT_STATE - .enable(SYSUI_STATE_OVERVIEW_DISABLED) - .enable(SYSUI_STATE_HOME_DISABLED) to false - ) + val stateToExpectedResult = + mapOf( + SYSUI_STATE_HOME_DISABLED to true, + SYSUI_STATE_OVERVIEW_DISABLED to true, + DEFAULT_STATE.enable(SYSUI_STATE_OVERVIEW_DISABLED) + .enable(SYSUI_STATE_HOME_DISABLED) to false, + ) stateToExpectedResult.forEach { (state, allowed) -> - underTest.setSystemUiFlags(state) + underTest.setSysUIStateFlags(state) assertThat(underTest.canStartTrackpadGesture()).isEqualTo(allowed) } } @@ -153,48 +181,59 @@ class RecentsAnimationDeviceStateTest { allSysUiStates().forEach { state -> val canStartGesture = !disablingStates.contains(state) - underTest.setSystemUiFlags(state) + underTest.setSysUIStateFlags(state) assertThat(underTest.canStartSystemGesture()).isEqualTo(canStartGesture) } } @Test fun systemGesturesNotAllowedWhenGestureStateDisabledAndNavBarVisible() { - val stateToExpectedResult = mapOf( - DEFAULT_STATE - .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, - DEFAULT_STATE - .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, - DEFAULT_STATE - .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, - DEFAULT_STATE - .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) - .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to false, - ) + val stateToExpectedResult = + mapOf( + DEFAULT_STATE.enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, + DEFAULT_STATE.enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, + DEFAULT_STATE.disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true, + DEFAULT_STATE.disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) + .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to false, + ) - stateToExpectedResult.forEach {(state, gestureAllowed) -> - underTest.setSystemUiFlags(state) + stateToExpectedResult.forEach { (state, gestureAllowed) -> + underTest.setSysUIStateFlags(state) assertThat(underTest.canStartSystemGesture()).isEqualTo(gestureAllowed) } } + @Test + fun startOverviewCommandForDisallowedSysUiState() { + val disallowedStates = GESTURE_DISABLING_SYSUI_STATES + SYSUI_STATE_OVERVIEW_DISABLED + + allSysUiStates().forEach { state -> + underTest.setSysUIStateFlags(state) + + val isAllowed = !disallowedStates.contains(state) + assertThat(underTest.canStartOverviewCommand()).isEqualTo(isAllowed) + } + } + private fun allSysUiStates(): List { // SYSUI_STATES_* are binary flags return (0..SYSUI_STATES_COUNT).map { 1L shl it } } companion object { - private val GESTURE_DISABLING_SYSUI_STATES = listOf( - SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, - SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING, - SYSUI_STATE_QUICK_SETTINGS_EXPANDED, - SYSUI_STATE_MAGNIFICATION_OVERLAP, - SYSUI_STATE_DEVICE_DREAMING, - SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION, - ) + private val GESTURE_DISABLING_SYSUI_STATES = + listOf( + SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, + SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING, + SYSUI_STATE_QUICK_SETTINGS_EXPANDED, + SYSUI_STATE_MAGNIFICATION_OVERLAP, + SYSUI_STATE_DEVICE_DREAMING, + SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION, + SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, + ) private const val SYSUI_STATES_COUNT = 33 private const val DEFAULT_STATE = 0L } diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java similarity index 60% rename from quickstep/tests/src/com/android/quickstep/RecentsModelTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java index 648fa932a6..42c585e1cf 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsModelTest.java @@ -32,22 +32,30 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; +import android.graphics.Rect; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.Flags; import com.android.launcher3.R; +import com.android.launcher3.graphics.ThemeManager; import com.android.launcher3.icons.IconProvider; +import com.android.launcher3.util.DaggerSingletonTracker; +import com.android.launcher3.util.LockedUserState; import com.android.quickstep.util.GroupTask; +import com.android.quickstep.util.SplitTask; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.wm.shell.shared.split.SplitBounds; +import com.android.wm.shell.shared.split.SplitScreenConstants; -import org.junit.After; 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.MockitoAnnotations; @@ -56,6 +64,7 @@ import java.util.ArrayList; import java.util.function.Consumer; @SmallTest +@RunWith(AndroidJUnit4.class) public class RecentsModelTest { @Mock private Context mContext; @@ -65,9 +74,16 @@ public class RecentsModelTest { @Mock private RecentTasksList mTasksList; + private RecentsModel.RecentTasksChangedListener mRegisteredTaskListListener = null; @Mock - private TaskThumbnailCache.HighResLoadingState mHighResLoadingState; + private HighResLoadingState mHighResLoadingState; + + @Mock + private LockedUserState mLockedUserState; + + @Mock + private ThemeManager mThemeManager; private RecentsModel mRecentsModel; @@ -88,13 +104,28 @@ public class RecentsModelTest { callback.accept(mTaskResult); return null; }).when(mTasksList).getTaskKeys(anyInt(), any()); + doAnswer(invocation -> { + mRegisteredTaskListListener = invocation.getArgument(0); + return null; + }).when(mTasksList).registerRecentTasksChangedListener(any()); + doAnswer(invocation -> { + mRegisteredTaskListListener = null; + return null; + }).when(mTasksList).unregisterRecentTasksChangedListener(); + doAnswer(invocation -> { + if (mRegisteredTaskListListener != null) { + mRegisteredTaskListListener.onRecentTasksChanged(); + } + return null; + }).when(mTasksList).onRecentTasksChanged(); when(mHighResLoadingState.isEnabled()).thenReturn(true); when(mThumbnailCache.getHighResLoadingState()).thenReturn(mHighResLoadingState); when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true); mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class), - mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class)); + mThumbnailCache, mock(IconProvider.class), mock(TaskStackChangeListeners.class), + mLockedUserState, () -> mThemeManager, mock(DaggerSingletonTracker.class)); mResource = mock(Resources.class); when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3); @@ -111,10 +142,12 @@ public class RecentsModelTest { .updateThumbnailInCache(taskArgs.capture(), /* lowResolution= */ eq(false)); GroupTask expectedGroupTask = mTaskResult.get(0); - assertThat(taskArgs.getAllValues().get(0)).isEqualTo( - expectedGroupTask.task1); - assertThat(taskArgs.getAllValues().get(1)).isEqualTo( - expectedGroupTask.task2); + var taskArgsValues = taskArgs.getAllValues(); + var expectedTasks = expectedGroupTask.getTasks(); + assertThat(taskArgsValues.size()).isEqualTo(expectedTasks.size()); + for (int i = 0; i < expectedTasks.size(); ++i) { + assertThat(taskArgsValues.get(i)).isEqualTo(expectedTasks.get(i)); + } } @Test @@ -157,6 +190,49 @@ public class RecentsModelTest { .updateThumbnailInCache(any(), anyBoolean()); } + @Test + public void themeCallbackAttachedOnUnlock() { + verify(mThemeManager, never()).addChangeListener(any()); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mLockedUserState).runOnUserUnlocked(callbackCaptor.capture()); + + callbackCaptor.getAllValues().forEach(Runnable::run); + verify(mThemeManager, times(1)).addChangeListener(any()); + } + + @Test + public void recentTaskListChangesNotiftListeners() { + RecentsModel.RecentTasksChangedListener listener1 = mock( + RecentsModel.RecentTasksChangedListener.class); + RecentsModel.RecentTasksChangedListener listener2 = mock( + RecentsModel.RecentTasksChangedListener.class); + + mRecentsModel.registerRecentTasksChangedListener(listener1); + + mTasksList.onRecentTasksChanged(); + verify(listener1, times(1)).onRecentTasksChanged(); + verify(listener2, times(0)).onRecentTasksChanged(); + + mRecentsModel.registerRecentTasksChangedListener(listener2); + + mTasksList.onRecentTasksChanged(); + verify(listener1, times(2)).onRecentTasksChanged(); + verify(listener2, times(1)).onRecentTasksChanged(); + + mRecentsModel.unregisterRecentTasksChangedListener(listener1); + + mTasksList.onRecentTasksChanged(); + verify(listener1, times(2)).onRecentTasksChanged(); + verify(listener2, times(2)).onRecentTasksChanged(); + + mRecentsModel.unregisterRecentTasksChangedListener(listener2); + + mTasksList.onRecentTasksChanged(); + verify(listener1, times(2)).onRecentTasksChanged(); + verify(listener2, times(2)).onRecentTasksChanged(); + } + private RecentTasksList.TaskLoadResult getTaskResult() { RecentTasksList.TaskLoadResult allTasks = new RecentTasksList.TaskLoadResult(0, false, 1); ActivityManager.RecentTaskInfo taskInfo1 = new ActivityManager.RecentTaskInfo(); @@ -167,7 +243,13 @@ public class RecentsModelTest { Task.TaskKey taskKey2 = new Task.TaskKey(taskInfo2); Task task2 = Task.from(taskKey2, taskInfo2, false); - allTasks.add(new GroupTask(task1, task2, null)); + allTasks.add( + new SplitTask(task1, task2, new SplitBounds( + /* leftTopBounds = */ new Rect(), + /* rightBottomBounds = */ new Rect(), + /* leftTopTaskId = */ 1, + /* rightBottomTaskId = */ 2, + /* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50))); return allTasks; } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java new file mode 100644 index 0000000000..399709d8a9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java @@ -0,0 +1,77 @@ +/* + * 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.quickstep; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.util.LauncherMultivalentJUnit; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsState; +import com.android.quickstep.fallback.window.RecentsWindowManager; +import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler; + +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@SmallTest +@RunWith(LauncherMultivalentJUnit.class) +public class RecentsWindowSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase< + RecentsState, + RecentsWindowManager, + FallbackRecentsView, + RecentsWindowSwipeHandler, + FallbackWindowInterface> { + + @Mock private FallbackRecentsView mRecentsView; + @Mock private RecentsWindowManager mRecentsWindowManager; + + @NonNull + @Override + protected RecentsWindowSwipeHandler createSwipeHandler(long touchTimeMs, + boolean continuingLastGesture) { + return new RecentsWindowSwipeHandler( + mContext, + mTaskAnimationManager, + mDeviceState, + mRotationTouchHelper, + mRecentsWindowManager, + mGestureState, + touchTimeMs, + continuingLastGesture, + mInputConsumerController, + mMSDLPlayerWrapper); + } + + @NonNull + @Override + protected RecentsWindowManager getRecentsContainer() { + return mRecentsWindowManager; + } + + @NonNull + @Override + protected FallbackRecentsView getRecentsView() { + return mRecentsView; + } + + @NonNull + @Override + protected RecentsState getBaseState() { + return RecentsState.BG_LAUNCHER; + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java new file mode 100644 index 0000000000..a831e82e67 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java @@ -0,0 +1,242 @@ +/* + * 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.quickstep; + +import static com.android.quickstep.TaskAnimationManager.RECENTS_ANIMATION_START_TIMEOUT_MS; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Display; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.window.flags.Flags; + +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.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TaskAnimationManagerTest { + private static final int EXTERNAL_DISPLAY_ID = 1; + protected final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + + @Mock + private SystemUiProxy mSystemUiProxy; + + private TaskAnimationManager mTaskAnimationManager; + private TaskAnimationManager mTaskAnimationManagerWithExternalDisplay; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTaskAnimationManager = new TaskAnimationManager(mContext, Display.DEFAULT_DISPLAY) { + @Override + SystemUiProxy getSystemUiProxy() { + return mSystemUiProxy; + } + }; + mTaskAnimationManagerWithExternalDisplay = + new TaskAnimationManager(mContext, EXTERNAL_DISPLAY_ID) { + @Override + SystemUiProxy getSystemUiProxy() { + return mSystemUiProxy; + } + }; + } + + @Test + public void startRecentsActivity_allowBackgroundLaunch() { + final LauncherActivityInterface activityInterface = mock(LauncherActivityInterface.class); + final GestureState gestureState = mock(GestureState.class); + final RecentsAnimationCallbacks.RecentsAnimationListener listener = + mock(RecentsAnimationCallbacks.RecentsAnimationListener.class); + doReturn(activityInterface).when(gestureState).getContainerInterface(); + runOnMainSync(() -> + mTaskAnimationManager.startRecentsAnimation(gestureState, new Intent(), listener)); + final ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(ActivityOptions.class); + verify(mSystemUiProxy) + .startRecentsActivity(any(), optionsCaptor.capture(), any(), anyBoolean(), + any(), anyInt()); + assertEquals(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS, + optionsCaptor.getValue().getPendingIntentBackgroundActivityStartMode()); + } + + @Test + public void testLauncherDestroyed_whileRecentsAnimationStartPending_finishesAnimation() { + final GestureState gestureState = buildMockGestureState(); + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(RecentsAnimationCallbacks.class); + final RecentsAnimationControllerCompat controllerCompat = + mock(RecentsAnimationControllerCompat.class); + final RemoteAnimationTarget remoteAnimationTarget = new RemoteAnimationTarget( + /* taskId= */ 0, + /* mode= */ RemoteAnimationTarget.MODE_CLOSING, + /* leash= */ new SurfaceControl(), + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + new Configuration().windowConfiguration, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + /* taskInfo= */ new ActivityManager.RunningTaskInfo(), + /* allowEnterPip= */ false); + + when(mSystemUiProxy + .startRecentsActivity(any(), any(), listenerCaptor.capture(), anyBoolean(), any(), + anyInt())) + .thenReturn(true); + + runOnMainSync(() -> { + mTaskAnimationManager.startRecentsAnimation( + gestureState, + new Intent(), + mock(RecentsAnimationCallbacks.RecentsAnimationListener.class)); + + // Simulate multiple launcher destroyed events before the recents animation start + mTaskAnimationManager.onLauncherDestroyed(); + mTaskAnimationManager.onLauncherDestroyed(); + mTaskAnimationManager.onLauncherDestroyed(); + listenerCaptor.getValue().onAnimationStart( + controllerCompat, + new RemoteAnimationTarget[] { remoteAnimationTarget }, + new RemoteAnimationTarget[] { remoteAnimationTarget }, + new Rect(), + new Rect(), + new Bundle(), + new TransitionInfo(0, 0)); + }); + + // Verify checks that finish was only called once + runOnMainSync(() -> verify(controllerCompat) + .finish(/* toHome= */ eq(false), anyBoolean(), any())); + } + + @Test + public void testRecentsAnimationStartTimeout_cleansUpRecentsAnimation() { + final GestureState gestureState = buildMockGestureState(); + when(mSystemUiProxy + .startRecentsActivity(any(), any(), any(), anyBoolean(), any(), anyInt())) + .thenReturn(true); + + runOnMainSync(() -> { + assertNull("Recents animation was started prematurely:", + mTaskAnimationManager.getCurrentCallbacks()); + + mTaskAnimationManager.startRecentsAnimation( + gestureState, + new Intent(), + mock(RecentsAnimationCallbacks.RecentsAnimationListener.class)); + + assertNotNull("TaskAnimationManager was cleaned up prematurely:", + mTaskAnimationManager.getCurrentCallbacks()); + }); + + SystemClock.sleep(RECENTS_ANIMATION_START_TIMEOUT_MS); + + runOnMainSync(() -> assertNull("TaskAnimationManager was not cleaned up after the timeout:", + mTaskAnimationManager.getCurrentCallbacks())); + } + + protected static void runOnMainSync(Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } + + private GestureState buildMockGestureState() { + final GestureState gestureState = mock(GestureState.class); + + doReturn(mock(LauncherActivityInterface.class)).when(gestureState).getContainerInterface(); + when(gestureState.getRunningTaskIds(anyBoolean())).thenReturn(new int[0]); + + return gestureState; + } + + /** + * Invokes maybeStartHomeAction on the given TaskAnimationManager and verifies whether the + * provided Runnable was invoked, based on the expectedResult. + * + * @param taskAnimationManager The TaskAnimationManager instance to test. + * @param expectedResult True if the Runnable is expected to be invoked, false otherwise. + */ + private void verifyCanStartHomeAction(TaskAnimationManager taskAnimationManager, + Boolean expectedResult) { + Runnable mockRunnable = mock(Runnable.class); + taskAnimationManager.maybeStartHomeAction(mockRunnable); + if (expectedResult) { + verify(mockRunnable).run(); + } else { + verify(mockRunnable, never()).run(); + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_REJECT_HOME_TRANSITION) + public void maybeStartHomeAction_withRejectHomeTransitionEnabled() { + verifyCanStartHomeAction(mTaskAnimationManager, true); + verifyCanStartHomeAction(mTaskAnimationManagerWithExternalDisplay, false); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_REJECT_HOME_TRANSITION) + public void maybeStartHomeAction_withRejectHomeTransitionDisabled() { + verifyCanStartHomeAction(mTaskAnimationManager, true); + verifyCanStartHomeAction(mTaskAnimationManagerWithExternalDisplay, true); + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskThumbnailCacheTest.java similarity index 96% rename from quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java rename to quickstep/tests/multivalentTests/src/com/android/quickstep/TaskThumbnailCacheTest.java index 4e04261e5c..3686c16ad1 100644 --- a/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskThumbnailCacheTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.R; @@ -35,12 +36,14 @@ import com.android.quickstep.util.TaskKeyCache; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.concurrent.Executor; @SmallTest +@RunWith(AndroidJUnit4.class) public class TaskThumbnailCacheTest { @Mock private Context mContext; diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/TopTaskTrackerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/TopTaskTrackerTest.kt new file mode 100644 index 0000000000..060862791f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/TopTaskTrackerTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.app.ActivityManager +import android.app.TaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.internal.R +import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND +import com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING +import com.android.wm.shell.shared.GroupedTaskInfo +import com.android.wm.shell.shared.GroupedTaskInfo.TYPE_DESK +import com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FULLSCREEN +import com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Test for [TopTaskTracker] */ +@RunWith(AndroidJUnit4::class) +class TopTaskTrackerTest { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + private val mockContext = mock() + private val mockResources = mock() + + @Before + fun setUp() { + doReturn(mockResources).whenever(mockContext).resources + } + + @Test + @EnableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingEnabled_noVisibleTasks() { + val cachedTaskInfo = TopTaskTracker.CachedTaskInfo(null) + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(null) + assertThat(result).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingEnabled_withVisibleTasks() { + val taskInfo = createTaskInfo(1, DEFAULT_DISPLAY) + val groupedTaskInfo = GroupedTaskInfo.forFullscreenTasks(taskInfo) + val cachedTaskInfo = TopTaskTracker.CachedTaskInfo(groupedTaskInfo) + + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(null) + assertThat(result).isEqualTo(groupedTaskInfo) + } + + @Test + @DisableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingDisabled_noTasks() { + val cachedTaskInfo = + TopTaskTracker.CachedTaskInfo( + emptyList(), + mockContext, + DEFAULT_DISPLAY, + INACTIVE_DESK_ID, + ) + + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(null) + + assertThat(result).isNull() + } + + @Test + @DisableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingDisabled_withFullscreenTask() { + val taskInfo = createTaskInfo(1, DEFAULT_DISPLAY) + val tasks = listOf(taskInfo) + val cachedTaskInfo = + TopTaskTracker.CachedTaskInfo(tasks, mockContext, DEFAULT_DISPLAY, INACTIVE_DESK_ID) + + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(null) + + assertThat(result).isNotNull() + assertThat(result!!.isBaseType(TYPE_FULLSCREEN)).isTrue() + assertThat(result.taskInfo1).isEqualTo(taskInfo) + } + + @Test + @DisableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingDisabled_withSplitTasks() { + val taskInfo1 = createTaskInfo(1, DEFAULT_DISPLAY) + val taskInfo2 = createTaskInfo(2, DEFAULT_DISPLAY) + val tasks = listOf(taskInfo1, taskInfo2) + val splitTaskIds = intArrayOf(1, 2) + val cachedTaskInfo = + TopTaskTracker.CachedTaskInfo(tasks, mockContext, DEFAULT_DISPLAY, INACTIVE_DESK_ID) + + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(splitTaskIds) + + assertThat(result).isNotNull() + assertThat(result!!.isBaseType(TYPE_SPLIT)).isTrue() + assertThat(result.taskInfo1).isEqualTo(taskInfo1) + assertThat(result.taskInfo2).isEqualTo(taskInfo2) + } + + @Test + @DisableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingDisabled_withDesktopEnabled_noActiveDesk() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(true) + .whenever(mockResources) + .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + val taskInfo = createDesktopTaskInfo(1, DEFAULT_DISPLAY) + val tasks = listOf(taskInfo) + val cachedTaskInfo = + TopTaskTracker.CachedTaskInfo(tasks, mockContext, DEFAULT_DISPLAY, INACTIVE_DESK_ID) + + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(null) + + assertThat(result).isNotNull() + assertThat(result!!.isBaseType(TYPE_FULLSCREEN)).isTrue() + assertThat(result.deskId).isEqualTo(INACTIVE_DESK_ID) + assertThat(result.taskInfo1).isEqualTo(taskInfo) + } + + @Test + @DisableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingDisabled_withDesktopEnabled_withActiveDesk() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(true) + .whenever(mockResources) + .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + val activeDeskId = 10 + val taskInfo = createDesktopTaskInfo(1, DEFAULT_DISPLAY) + val tasks = listOf(taskInfo) + val cachedTaskInfo = + TopTaskTracker.CachedTaskInfo(tasks, mockContext, DEFAULT_DISPLAY, activeDeskId) + + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(null) + + assertThat(result).isNotNull() + assertThat(result!!.isBaseType(TYPE_DESK)).isTrue() + assertThat(result.deskId).isEqualTo(activeDeskId) + assertThat(result.getTaskInfoList()).isEqualTo(tasks) + } + + @Test + @DisableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND) + fun getPlaceholderGroupedTaskInfo_shellTopTaskTrackingDisabled_withDesktopDisabled_withActiveDesk() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(true) + .whenever(mockResources) + .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + val taskInfo = createDesktopTaskInfo(1, DEFAULT_DISPLAY) + val tasks = listOf(taskInfo) + val cachedTaskInfo = + TopTaskTracker.CachedTaskInfo(tasks, mockContext, DEFAULT_DISPLAY, INACTIVE_DESK_ID) + val result = cachedTaskInfo.getPlaceholderGroupedTaskInfo(null) + + assertThat(result).isNotNull() + assertThat(result!!.isBaseType(TYPE_DESK)).isTrue() + assertThat(result.deskId).isEqualTo(INACTIVE_DESK_ID) + assertThat(result.taskInfo1).isEqualTo(taskInfo) + } + + private fun createTaskInfo(taskId: Int, displayId: Int): TaskInfo { + val taskInfo = ActivityManager.RunningTaskInfo() + taskInfo.taskId = taskId + taskInfo.displayId = displayId + taskInfo.baseIntent = Intent() + taskInfo.baseActivity = ComponentName("test", "test") + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD) + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN) + return taskInfo + } + + private fun createDesktopTaskInfo(taskId: Int, displayId: Int): TaskInfo { + val taskInfo = createTaskInfo(taskId, displayId) + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM) + return taskInfo + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/actioncorner/ActionCornerHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/actioncorner/ActionCornerHandlerTest.kt new file mode 100644 index 0000000000..c3b62303a5 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/actioncorner/ActionCornerHandlerTest.kt @@ -0,0 +1,228 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.actioncorner + +import android.app.ActivityManager.RecentTaskInfo +import android.graphics.Rect +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import android.window.IWindowContainerToken +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.launcher3.Flags.FLAG_ENABLE_REVERSIBLE_HOME_ACTION_CORNER +import com.android.launcher3.LauncherState +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.TestUtil +import com.android.quickstep.BaseActivityInterface +import com.android.quickstep.OverviewCommandHelper +import com.android.quickstep.OverviewComponentObserver +import com.android.quickstep.RecentsModel +import com.android.quickstep.TopTaskTracker +import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SplitSelectStateController +import com.android.quickstep.util.SplitTask +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.system.ActivityManagerWrapper +import com.android.systemui.shared.system.actioncorner.ActionCornerConstants.HOME +import com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING +import com.android.wm.shell.shared.GroupedTaskInfo +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants +import java.util.function.Consumer +import java.util.function.Predicate +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(LauncherMultivalentJUnit::class) +@DisableFlags(FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) +@EnableFlags(FLAG_ENABLE_REVERSIBLE_HOME_ACTION_CORNER) +class ActionCornerHandlerTest { + + @get:Rule val context = SandboxApplication() + @get:Rule val mSetFlagsRule = SetFlagsRule() + + private val overviewCommandHelper: OverviewCommandHelper = mock() + private val overviewComponentObserver: OverviewComponentObserver = mock() + private val containerInterface: BaseActivityInterface = mock() + private val recentsViewContainer: QuickstepLauncher = mock() + private val splitSelectStateController: SplitSelectStateController = mock() + + private val topTaskTracker: TopTaskTracker = mock() + + private val recentsModel: RecentsModel = mock() + private val activityManagerWrapper: ActivityManagerWrapper = mock() + + private val bgExecutor = UI_HELPER_EXECUTOR + private val topTask: TopTaskTracker.CachedTaskInfo = mock() + private val actionCornerHandler: ActionCornerHandler = + ActionCornerHandler( + context, + overviewComponentObserver, + topTaskTracker, + recentsModel, + activityManagerWrapper, + bgExecutor, + overviewCommandHelper, + ) + + @Before + fun setup() { + whenever(topTaskTracker.getCachedTopTask(false, DEFAULT_DISPLAY)).thenReturn(topTask) + whenever(containerInterface.getCreatedContainer()).thenReturn(recentsViewContainer) + whenever(overviewComponentObserver.getContainerInterface(any())) + .thenReturn(containerInterface) + whenever(recentsViewContainer.lifecycle).thenReturn(mock()) + whenever(recentsViewContainer.splitSelectStateController) + .thenReturn(splitSelectStateController) + } + + @Test + fun inSingleTask_homeActionCornerTwice_goHomeThenGoBackToSingleTask() { + whenever(topTask.isHomeTask).thenReturn(false) + val taskInfo = createTaskInfo(1) + mockFullScreenTopTask(taskInfo) + + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + + verify(overviewCommandHelper) + .addCommand(OverviewCommandHelper.CommandType.HOME, DEFAULT_DISPLAY) + + // Toggle back to the task from home + mockAtHome() + mockRecentsModelTasks(listOf(SingleTask(Task.from(taskInfo)))) + + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + + TestUtil.runOnExecutorSync(bgExecutor) {} + verify(activityManagerWrapper).startActivityFromRecents(eq(1), any()) + } + + @Test + fun inSplitTask_homeActionCornerTwice_goHomeThenGoBackToSplitTask() { + whenever(topTask.isHomeTask).thenReturn(false) + val taskInfo1 = createTaskInfo(1) + val taskInfo2 = createTaskInfo(2) + mockSplitTopTask(taskInfo1, taskInfo2) + + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + + verify(overviewCommandHelper) + .addCommand(OverviewCommandHelper.CommandType.HOME, DEFAULT_DISPLAY) + + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + + mockAtHome() + val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SplitScreenConstants.SNAP_TO_2_33_66) + mockRecentsModelTasks( + listOf(SplitTask(Task.from(taskInfo1), Task.from(taskInfo2), splitBounds)) + ) + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + + verify(splitSelectStateController) + .launchExistingSplitPair( + eq(null), + eq(taskInfo1.taskId), + eq(taskInfo2.taskId), + anyInt(), + any(), + eq(false), + eq(SplitScreenConstants.SNAP_TO_2_33_66), + ) + } + + @Test + fun atHome_homeActionCorner_doNothing() { + mockAtHome() + + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + + verify(overviewCommandHelper, never()) + .addCommand(OverviewCommandHelper.CommandType.HOME, DEFAULT_DISPLAY) + verify(activityManagerWrapper, never()).startActivityFromRecents(eq(1), any()) + } + + @Test + fun inSingleTask_homeActionCorner_changeTask_goHome_homeActionCorner_doNothing() { + whenever(topTask.isHomeTask).thenReturn(false) + val taskInfo = createTaskInfo(1) + mockFullScreenTopTask(taskInfo) + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + + // Change task + val taskInfo2 = createTaskInfo(2) + mockFullScreenTopTask(taskInfo2) + mockRecentsModelTasks(listOf(SingleTask(Task.from(taskInfo2)))) + mockAtHome() + + // Do not go back to any task because the latest task was changed after going home by + // action corner + actionCornerHandler.handleAction(HOME, DEFAULT_DISPLAY) + verify(activityManagerWrapper, never()).startActivityFromRecents(anyInt(), any()) + } + + private fun mockFullScreenTopTask(taskInfo: RecentTaskInfo) { + val groupedTask = GroupedTaskInfo.forFullscreenTasks(taskInfo) + whenever(topTaskTracker.runningSplitTaskIds).thenReturn(intArrayOf()) + whenever(topTask.getPlaceholderGroupedTaskInfo(any())).thenReturn(groupedTask) + } + + private fun mockSplitTopTask(task1: RecentTaskInfo, task2: RecentTaskInfo) { + val splitGroupedTask = GroupedTaskInfo.forSplitTasks(task1, task2, null) + whenever(topTaskTracker.runningSplitTaskIds) + .thenReturn(intArrayOf(task1.getTaskId(), task2.getTaskId())) + whenever(topTask.getPlaceholderGroupedTaskInfo(any())).thenReturn(splitGroupedTask) + } + + private fun mockAtHome() { + whenever(topTask.isHomeTask).thenReturn(true) + whenever(recentsViewContainer.isRecentsViewVisible).thenReturn(false) + } + + private fun mockRecentsModelTasks(tasks: List) { + whenever(recentsModel.getTasks(any(), any>>())).thenAnswer { + invocation -> + val filter = invocation.getArgument>(0) + val callback = invocation.getArgument>>(1) + callback.accept(ArrayList(tasks.stream().filter(filter).toList())) + 0 + } + } + + private fun createTaskInfo(id: Int) = + RecentTaskInfo().apply { + taskId = id + displayId = DEFAULT_DISPLAY + token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/fallback/RecentsStateUtilsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/fallback/RecentsStateUtilsTest.kt new file mode 100644 index 0000000000..4e9dae8da0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/fallback/RecentsStateUtilsTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.fallback + +import com.android.launcher3.testing.shared.TestProtocol.BACKGROUND_APP_STATE_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_MODAL_TASK_STATE_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL +import com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelTablet2023"]) +class RecentsStateUtilsTest { + + @Test + fun testRecentsStateDefault_toLauncherStateOrdinal_isOverviewStateOrdinal() { + assertThat(RecentsState.DEFAULT.toLauncherStateOrdinal()).isEqualTo(OVERVIEW_STATE_ORDINAL) + } + + @Test + fun testRecentsStateModal_toLauncherStateOrdinal_isModalTaskStateOrdinal() { + assertThat(RecentsState.MODAL_TASK.toLauncherStateOrdinal()) + .isEqualTo(OVERVIEW_MODAL_TASK_STATE_ORDINAL) + } + + @Test + fun testRecentsStateBackgroundApp_toLauncherStateOrdinal_isBackgroundAppStateOrdinal() { + assertThat(RecentsState.BACKGROUND_APP.toLauncherStateOrdinal()) + .isEqualTo(BACKGROUND_APP_STATE_ORDINAL) + } + + @Test + fun testRecentsStateHome_toLauncherStateOrdinal_isNormalStateOrdinal() { + assertThat(RecentsState.HOME.toLauncherStateOrdinal()).isEqualTo(NORMAL_STATE_ORDINAL) + } + + @Test + fun testRecentsStateBgLauncher_toLauncherStateOrdinal_isNormalStateOrdinal() { + assertThat(RecentsState.BG_LAUNCHER.toLauncherStateOrdinal()) + .isEqualTo(NORMAL_STATE_ORDINAL) + } + + @Test + fun testRecentsStateOverviewSplitSelect_toLauncherStateOrdinal_isOverviewSplitSelectStateOrdinal() { + assertThat(RecentsState.OVERVIEW_SPLIT_SELECT.toLauncherStateOrdinal()) + .isEqualTo(OVERVIEW_SPLIT_SELECT_ORDINAL) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/input/QuickstepKeyGestureEventsHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/input/QuickstepKeyGestureEventsHandlerTest.kt new file mode 100644 index 0000000000..be1b2949a6 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/input/QuickstepKeyGestureEventsHandlerTest.kt @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.input + +import android.app.PendingIntent +import android.hardware.input.InputManager +import android.hardware.input.KeyGestureEvent +import android.hardware.input.KeyGestureEvent.ACTION_GESTURE_COMPLETE +import android.hardware.input.KeyGestureEvent.ACTION_GESTURE_START +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.util.SandboxApplication +import com.android.quickstep.input.QuickstepKeyGestureEventsHandlerTest.FakeOverviewHandler.OverviewEvent +import com.android.quickstep.input.QuickstepKeyGestureEventsManager.OverviewGestureHandler +import com.android.quickstep.input.QuickstepKeyGestureEventsManager.OverviewGestureHandler.OverviewType +import com.android.quickstep.input.QuickstepKeyGestureEventsManager.OverviewGestureHandler.OverviewType.ALT_TAB +import com.android.quickstep.input.QuickstepKeyGestureEventsManager.OverviewGestureHandler.OverviewType.UNDEFINED +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.KArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class QuickstepKeyGestureEventsHandlerTest { + @get:Rule val context = SandboxApplication() + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + private val inputManager = context.spyService(InputManager::class.java) + private val allAppsPendingIntent: PendingIntent = mock() + private val keyGestureEventsCaptor: KArgumentCaptor> = argumentCaptor() + private val fakeOverviewHandler = FakeOverviewHandler() + private lateinit var keyGestureEventsManager: QuickstepKeyGestureEventsManager + + @Before + fun setup() { + doNothing().whenever(inputManager).registerKeyGestureEventHandler(any(), any()) + doNothing().whenever(inputManager).unregisterKeyGestureEventHandler(any()) + keyGestureEventsManager = QuickstepKeyGestureEventsManager(context) + keyGestureEventsManager.onUserSetupCompleteListener.onSettingsChanged(/* isEnabled= */ true) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun registerAllAppsHandler_flagEnabled_registerWithExpectedKeyGestureEvents() { + keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent) + + verify(inputManager) + .registerKeyGestureEventHandler( + keyGestureEventsCaptor.capture(), + eq(keyGestureEventsManager.allAppsKeyGestureEventHandler), + ) + assertThat(keyGestureEventsCaptor.firstValue).containsExactly(KEY_GESTURE_TYPE_ALL_APPS) + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun registerAllAppsHandler_flagDisabled_noRegister() { + keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent) + + verifyNoInteractions(inputManager) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun registerOverviewHandler_flagEnabled_registerWithExpectedKeyGestureEvents() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + verify(inputManager) + .registerKeyGestureEventHandler( + keyGestureEventsCaptor.capture(), + eq(keyGestureEventsManager.overviewKeyGestureEventHandler), + ) + assertThat(keyGestureEventsCaptor.firstValue) + .containsExactly(KEY_GESTURE_TYPE_RECENT_APPS, KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER) + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun registerOverviewHandler_flagDisabled_noRegister() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + verifyNoInteractions(inputManager) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun unregisterAllAppsHandler_flagEnabled_unregisterHandler() { + keyGestureEventsManager.unregisterAllAppsKeyGestureEvent() + + verify(inputManager) + .unregisterKeyGestureEventHandler( + eq(keyGestureEventsManager.allAppsKeyGestureEventHandler) + ) + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun unregisterAllAppsHandler_flagDisabled_noUnregister() { + keyGestureEventsManager.unregisterAllAppsKeyGestureEvent() + + verifyNoInteractions(inputManager) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun unregisterOverviewHandler_flagEnabled_unregisterHandler() { + keyGestureEventsManager.unregisterOverviewKeyGestureEvent() + + verify(inputManager) + .unregisterKeyGestureEventHandler( + eq(keyGestureEventsManager.overviewKeyGestureEventHandler) + ) + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun unregisterOverviewHandler_flagDisabled_noUnregister() { + keyGestureEventsManager.unregisterOverviewKeyGestureEvent() + + verifyNoInteractions(inputManager) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleAllAppsEvent_flagEnabled_toggleAllAppsSearch() { + keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent) + + keyGestureEventsManager.allAppsKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS) + .build(), + /* focusedToken= */ null, + ) + + verify(allAppsPendingIntent).send() + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleAllAppsEvent_flagEnabled_userSetupIncomplete_noInteractionWithTaskbar() { + keyGestureEventsManager.onUserSetupCompleteListener.onSettingsChanged( + /* isEnabled= */ false + ) + keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent) + + keyGestureEventsManager.allAppsKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS) + .build(), + /* focusedToken= */ null, + ) + + verifyNoInteractions(allAppsPendingIntent) + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleAllAppsEvent_flagDisabled_noInteractionWithTaskbar() { + keyGestureEventsManager.registerAllAppsKeyGestureEvent(allAppsPendingIntent) + + keyGestureEventsManager.allAppsKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS) + .build(), + /* focusedToken= */ null, + ) + + verifyNoInteractions(allAppsPendingIntent) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsEvent_flagEnabled_showOverviewWithUndefinedType() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS) + .setAction(ACTION_GESTURE_COMPLETE) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent) + .isEqualTo(OverviewEvent(shouldShowOverview = true, type = UNDEFINED)) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsEvent_userSetupIncomplete_noOverviewEventInFake() { + keyGestureEventsManager.onUserSetupCompleteListener.onSettingsChanged( + /* isEnabled= */ false + ) + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS) + .setAction(ACTION_GESTURE_COMPLETE) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsEvent_flagDisabled_noOverviewEventInFake() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS) + .setAction(ACTION_GESTURE_COMPLETE) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsSwitcherStartEvent_flagEnabled_showOverviewWithAltTabType() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER) + .setAction(ACTION_GESTURE_START) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent) + .isEqualTo(OverviewEvent(shouldShowOverview = true, type = ALT_TAB)) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsSwitcherStartEvent_userSetupIncomplete_noOverviewEventInFake() { + keyGestureEventsManager.onUserSetupCompleteListener.onSettingsChanged( + /* isEnabled= */ false + ) + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER) + .setAction(ACTION_GESTURE_START) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsSwitcherStartEvent_flagDisabled_noOverviewEventInFake() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER) + .setAction(ACTION_GESTURE_START) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsSwitcherCompleteEvent_flagEnabled_hideOverviewWithAltTabType() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER) + .setAction(ACTION_GESTURE_COMPLETE) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent) + .isEqualTo(OverviewEvent(shouldShowOverview = false, type = ALT_TAB)) + } + + @Test + @EnableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsSwitcherCompleteEvent_userSetupIncomplete_noOverviewEventInFake() { + keyGestureEventsManager.onUserSetupCompleteListener.onSettingsChanged( + /* isEnabled= */ false + ) + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER) + .setAction(ACTION_GESTURE_COMPLETE) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_GRANT_MANAGE_KEY_GESTURES_TO_RECENTS) + fun handleRecentAppsSwitcherCompleteEvent_flagDisabled_noOverviewEventInFake() { + keyGestureEventsManager.registerOverviewKeyGestureEvent(fakeOverviewHandler) + + keyGestureEventsManager.overviewKeyGestureEventHandler.handleKeyGestureEvent( + KeyGestureEvent.Builder() + .setDisplayId(TEST_DISPLAY_ID) + .setKeyGestureType(KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER) + .setAction(ACTION_GESTURE_COMPLETE) + .build(), + /* focusedToken= */ null, + ) + + assertThat(fakeOverviewHandler.overviewEvent).isNull() + } + + private class FakeOverviewHandler : OverviewGestureHandler { + data class OverviewEvent(val shouldShowOverview: Boolean, val type: OverviewType) + + var overviewEvent: OverviewEvent? = null + private set + + override fun showOverview(type: OverviewType) { + overviewEvent = OverviewEvent(shouldShowOverview = true, type) + } + + override fun hideOverview(type: OverviewType) { + overviewEvent = OverviewEvent(shouldShowOverview = false, type) + } + } + + private companion object { + const val TEST_DISPLAY_ID = 6789 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java new file mode 100644 index 0000000000..f8a0c080a6 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java @@ -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. + */ + +package com.android.quickstep.inputconsumers; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.util.VibratorWrapper; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.NavHandle; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.util.ContextualSearchHapticManager; +import com.android.quickstep.util.ContextualSearchInvoker; +import com.android.quickstep.util.ContextualSearchStateManager; +import com.android.quickstep.util.TestExtensions; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NavHandleLongPressHandlerTest { + + private NavHandleLongPressHandler mLongPressHandler; + @Mock private NavHandle mNavHandle; + @Mock VibratorWrapper mVibratorWrapper; + @Mock ContextualSearchHapticManager mContextualSearchHapticManager; + @Mock TopTaskTracker mTopTaskTracker; + @Mock StatsLogManager.StatsLogManagerFactory mStatsLogManagerFactory; + @Mock StatsLogManager mStatsLogManager; + @Mock ContextualSearchStateManager mContextualSearchStateManager; + @Mock ContextualSearchInvoker mContextualSearchInvoker; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getInstrumentation() + .getTargetContext().getApplicationContext(); + when(mStatsLogManagerFactory.create(context)).thenReturn(mStatsLogManager); + mLongPressHandler = new NavHandleLongPressHandler(context, mVibratorWrapper, + mContextualSearchHapticManager, mTopTaskTracker, mStatsLogManagerFactory, + mContextualSearchStateManager, mContextualSearchInvoker); + } + + @Test + public void testStartNavBarAnimation_flagDisabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(false)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle, never()) + .animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testStartNavBarAnimation_flagEnabled() { + try (AutoCloseable flag = overrideAnimateLPNHFlag(true)) { + mLongPressHandler.startNavBarAnimation(mNavHandle); + verify(mNavHandle).animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private AutoCloseable overrideAnimateLPNHFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ANIMATE_LPNH", value, () -> DeviceConfigWrapper.get().getAnimateLpnh()); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java new file mode 100644 index 0000000000..ee9505cdbb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java @@ -0,0 +1,545 @@ +/* + * 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.quickstep.inputconsumers; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_HOVER_ENTER; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR; +import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS; +import static com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer.MIN_TIME_TO_LOG_ABANDON_MS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.os.SystemClock; +import android.view.MotionEvent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.util.AllModulesForTest; +import com.android.launcher3.util.DisplayController; +import com.android.launcher3.util.SandboxContext; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.GestureState; +import com.android.quickstep.InputConsumer; +import com.android.quickstep.NavHandle; +import com.android.quickstep.RecentsAnimationDeviceState; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.util.TestExtensions; +import com.android.systemui.shared.system.InputMonitorCompat; + +import dagger.BindsInstance; +import dagger.Component; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.atomic.AtomicBoolean; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NavHandleLongPressInputConsumerTest { + + private static final float TOUCH_SLOP = 10; + private static final float SQUARED_TOUCH_SLOP = 100; + + private final AtomicBoolean mLongPressTriggered = new AtomicBoolean(); + private final Runnable mLongPressRunnable = () -> mLongPressTriggered.set(true); + private NavHandleLongPressInputConsumer mUnderTest; + private SandboxContext mContext; + private float mScreenWidth; + private long mDownTimeMs; + @Mock InputConsumer mDelegate; + @Mock InputMonitorCompat mInputMonitor; + @Mock RecentsAnimationDeviceState mDeviceState; + @Mock NavHandle mNavHandle; + @Mock GestureState mGestureState; + @Mock NavHandleLongPressHandler mNavHandleLongPressHandler; + @Mock TopTaskTracker mTopTaskTracker; + @Mock TopTaskTracker.CachedTaskInfo mTaskInfo; + @Mock StatsLogManager mStatsLogManager; + @Mock StatsLogManager.StatsLogger mStatsLogger; + @Mock StatsLogManager.StatsLatencyLogger mStatsLatencyLogger; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mTopTaskTracker.getCachedTopTask(anyBoolean(), anyInt())).thenReturn(mTaskInfo); + when(mDeviceState.getSquaredTouchSlop()).thenReturn(SQUARED_TOUCH_SLOP); + when(mDelegate.allowInterceptByParent()).thenReturn(true); + mLongPressTriggered.set(false); + when(mNavHandleLongPressHandler.getLongPressRunnable(any(), anyInt())).thenReturn( + mLongPressRunnable); + when(mStatsLogger.withPackageName(any())).thenReturn(mStatsLogger); + when(mStatsLatencyLogger.withInstanceId(any())).thenReturn(mStatsLatencyLogger); + when(mStatsLatencyLogger.withLatency(anyLong())).thenReturn(mStatsLatencyLogger); + when(mStatsLogManager.logger()).thenReturn(mStatsLogger); + when(mStatsLogManager.latencyLogger()).thenReturn(mStatsLatencyLogger); + initializeObjectUnderTest(); + } + + @After + public void tearDown() throws Exception { + MAIN_EXECUTOR.getHandler().removeCallbacks(mLongPressRunnable); + MAIN_EXECUTOR.submit(() -> null).get(); + mContext.onDestroy(); + } + + @Test + public void testGetType() { + assertThat(mUnderTest.getType() & InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS).isNotEqualTo(0); + } + + @Test + public void testDelegateDisallowsTouchIntercept() { + when(mDelegate.allowInterceptByParent()).thenReturn(false); + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + + verify(mDelegate).onMotionEvent(any()); + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + verify(mNavHandleLongPressHandler, never()).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testDelegateDisallowsTouchInterceptAfterTouchDown() { + // Touch down and wait the minimum abandonment time. + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + // Delegate should still get touches unless long press is triggered. + verify(mDelegate).onMotionEvent(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + + // Child delegate blocks us from intercepting further motion events. + when(mDelegate.allowInterceptByParent()).thenReturn(false); + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_MOVE)); + + // Delegate should still get motion events unless long press is triggered. + verify(mDelegate, times(2)).onMotionEvent(any()); + // But our handler should be cancelled. + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + // Because we handled touch down before the child blocked additional events, log abandon. + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressTriggered() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + + // Ensure abandon latency is still not logged after long press. + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP)); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressTriggeredWithSlightVerticalMovement() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, 1)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressTriggeredWithSlightHorizontalMovement() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, mScreenWidth / 2f + 1, 0)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressTriggeredWithExtendedTwoStageDuration() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP - 1), 0)); + // We have entered the second stage, so the normal timeout shouldn't trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + + // After an extended time, the long press should trigger. + float extendedDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongPressTriggeredWithNormalDurationInFirstStage() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + // We have not entered the second stage, so the normal timeout should trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE); + assertTrue(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR); + verifyNoMoreInteractions(mStatsLatencyLogger); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongPressAbortedByTouchUp() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP)); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressAbortedByTouchCancel() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL)); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testTouchCancelWithoutTouchDown() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL)); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, never()).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedVertically() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, + -(TOUCH_SLOP + 1))); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedHorizontally() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(MIN_TIME_TO_LOG_ABANDON_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP + 1), 0)); + // Wait past the long press timeout, to be extra sure it wouldn't have triggered. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } + + @Test + public void testLongPressAbortedByTouchSlopPassedVertically_twoStageEnabled() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + // Enter the second stage. + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, + -(TOUCH_SLOP - 1))); + // Normal duration shouldn't trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + // Move out of the second stage. + mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, + -(TOUCH_SLOP + 1))); + // Wait past the extended long press timeout, to be sure it wouldn't have triggered. + float extendedDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + // Touch cancelled. + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testLongPressAbortedByTouchSlopPassedHorizontally_twoStageEnabled() { + try (AutoCloseable flag = overrideTwoStageFlag(true)) { + // Reinitialize to pick up updated flag state. + initializeObjectUnderTest(); + + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + // Enter the second stage. + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP - 1), 0)); + // Normal duration shouldn't trigger. + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + + // Move out of the second stage. + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, + mScreenWidth / 2f - (TOUCH_SLOP + 1), 0)); + // Wait past the extended long press timeout, to be sure it wouldn't have triggered. + float extendedDurationMultiplier = + (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f); + sleep((long) (DEFAULT_LPNH_TIMEOUT_MS + * (extendedDurationMultiplier - 1))); // -1 because we already waited 1x + + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any()); + // Touch cancelled. + verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogger); + verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testTouchOutsideNavHandleIgnored() { + // Touch the far left side of the screen. (y=0 is top of navbar region, picked arbitrarily) + mUnderTest.onMotionEvent(generateMotionEvent(ACTION_DOWN, 0, 0)); + sleep(DEFAULT_LPNH_TIMEOUT_MS); + + // Should be ignored because the x position was not centered in the navbar region. + assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE); + assertFalse(mLongPressTriggered.get()); + verify(mNavHandleLongPressHandler, never()).onTouchStarted(any()); + verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any()); + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testHoverPassedToDelegate() { + // Regardless of whether the delegate wants us to intercept, we tell it about hover events. + when(mDelegate.allowInterceptByParent()).thenReturn(false); + mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER)); + + verify(mDelegate).onHoverEvent(any()); + + when(mDelegate.allowInterceptByParent()).thenReturn(true); + mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER)); + + verify(mDelegate, times(2)).onHoverEvent(any()); + + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + @Test + public void testNoLogsForShortTouch() { + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN)); + sleep(10); + mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP)); + verifyNoMoreInteractions(mStatsLogManager); + verifyNoMoreInteractions(mStatsLogger); + verifyNoMoreInteractions(mStatsLatencyLogger); + } + + private void initializeObjectUnderTest() { + if (mContext != null) { + mContext.onDestroy(); + } + mContext = new SandboxContext(getApplicationContext()); + mContext.initDaggerComponent( + DaggerNavHandleLongPressInputConsumerTest_TopTaskTrackerComponent + .builder() + .bindTopTaskTracker(mTopTaskTracker)); + mScreenWidth = DisplayController.INSTANCE.get(mContext).getInfo().currentSize.x; + mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor, + mDeviceState, mNavHandle, mGestureState); + mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler); + mUnderTest.setStatsLogManager(mStatsLogManager); + mDownTimeMs = 0; + } + + private static void sleep(long sleepMs) { + SystemClock.sleep(sleepMs); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + /** Generate a motion event centered horizontally in the screen. */ + private MotionEvent generateCenteredMotionEvent(int motionAction) { + return generateCenteredMotionEventWithYOffset(motionAction, 0); + } + + /** Generate a motion event centered horizontally in the screen, with y offset. */ + private MotionEvent generateCenteredMotionEventWithYOffset(int motionAction, float y) { + return generateMotionEvent(motionAction, mScreenWidth / 2f, y); + } + + private MotionEvent generateMotionEvent(int motionAction, float x, float y) { + if (motionAction == ACTION_DOWN) { + mDownTimeMs = SystemClock.uptimeMillis(); + } + long eventTime = SystemClock.uptimeMillis(); + return MotionEvent.obtain(mDownTimeMs, eventTime, motionAction, x, y, 0); + } + + private static AutoCloseable overrideTwoStageFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ENABLE_LPNH_TWO_STAGES", + value, + () -> DeviceConfigWrapper.get().getEnableLpnhTwoStages()); + } + + @LauncherAppSingleton + @Component(modules = AllModulesForTest.class) + public interface TopTaskTrackerComponent extends LauncherAppComponent { + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance Builder bindTopTaskTracker(TopTaskTracker topTaskTracker); + + @Override + TopTaskTrackerComponent build(); + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt new file mode 100644 index 0000000000..5e22012b45 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt @@ -0,0 +1,179 @@ +/* + * 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.quickstep.logging + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.LauncherPrefs.Companion.ALLOW_ROTATION +import com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY +import com.android.launcher3.graphics.ThemeManager +import com.android.launcher3.logging.InstanceId +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_DISABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_ROTATION_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED +import com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.SettingsCache +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Answers +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.capture +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class SettingsChangeLoggerTest { + + @get:Rule val mockito = MockitoJUnit.rule() + + private val mContext: Context = ApplicationProvider.getApplicationContext() + + private val mInstanceId = InstanceId.fakeInstanceId(1) + + private lateinit var mSystemUnderTest: SettingsChangeLogger + + @Mock private lateinit var mStatsLogFactory: StatsLogManager.StatsLogManagerFactory + + @Mock private lateinit var mStatsLogManager: StatsLogManager + + @Mock(answer = Answers.RETURNS_SELF) + private lateinit var mMockLogger: StatsLogManager.StatsLogger + @Mock private lateinit var mTracker: DaggerSingletonTracker + private var displayController: DisplayController = DisplayController.INSTANCE.get(mContext) + private var settingsCache: SettingsCache = SettingsCache.INSTANCE.get(mContext) + + @Captor private lateinit var mEventCaptor: ArgumentCaptor + + private var mDefaultThemedIcons = false + private var mDefaultAllowRotation = false + + private val themeManager: ThemeManager + get() = ThemeManager.INSTANCE.get(mContext) + + @Before + fun setUp() { + whenever(mStatsLogFactory.create(mContext)).doReturn(mStatsLogManager) + whenever(mStatsLogManager.logger()).doReturn(mMockLogger) + mDefaultThemedIcons = themeManager.isMonoThemeEnabled + mDefaultAllowRotation = LauncherPrefs.get(mContext).get(ALLOW_ROTATION) + // To match the default value of THEMED_ICONS + themeManager.isMonoThemeEnabled = false + // To match the default value of ALLOW_ROTATION + LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false) + + mSystemUnderTest = + SettingsChangeLogger( + mContext, + mTracker, + displayController, + settingsCache, + mStatsLogFactory, + ) + } + + @After + fun tearDown() { + themeManager.isMonoThemeEnabled = mDefaultThemedIcons + LauncherPrefs.get(mContext).put(ALLOW_ROTATION, mDefaultAllowRotation) + } + + @Test + fun loggingPrefs_correctDefaultValue() { + val systemUnderTest = + SettingsChangeLogger( + mContext, + mTracker, + displayController, + settingsCache, + mStatsLogFactory, + ) + + assertThat(systemUnderTest.loggingPrefs[ALLOW_ROTATION_PREFERENCE_KEY]!!.defaultValue) + .isFalse() + assertThat(systemUnderTest.loggingPrefs[ADD_ICON_PREFERENCE_KEY]!!.defaultValue).isTrue() + assertThat(systemUnderTest.loggingPrefs[OVERVIEW_SUGGESTED_ACTIONS]!!.defaultValue).isTrue() + assertThat(systemUnderTest.loggingPrefs[KEY_ENABLE_MINUS_ONE]!!.defaultValue).isTrue() + } + + @Test + fun logSnapshot_defaultValue() { + mSystemUnderTest.logSnapshot(mInstanceId) + + verify(mMockLogger, atLeastOnce()).log(capture(mEventCaptor)) + val capturedEvents = mEventCaptor.allValues + assertThat(capturedEvents.isNotEmpty()).isTrue() + verifyDefaultEvent(capturedEvents) + assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_DISABLED.id }) + .isTrue() + } + + @Test + fun logSnapshot_updateAllowRotation() { + LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = true) + + // This a new object so the values of mLoggablePrefs will be different + SettingsChangeLogger(mContext, mTracker, displayController, settingsCache, mStatsLogFactory) + .logSnapshot(mInstanceId) + + verify(mMockLogger, atLeastOnce()).log(capture(mEventCaptor)) + val capturedEvents = mEventCaptor.allValues + assertThat(capturedEvents.isNotEmpty()).isTrue() + verifyDefaultEvent(capturedEvents) + assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_ROTATION_ENABLED.id }) + .isTrue() + } + + private fun verifyDefaultEvent(capturedEvents: MutableList) { + assertThat(capturedEvents.any { it.id == LAUNCHER_NOTIFICATION_DOT_ENABLED.id }).isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_THEMED_ICON_DISABLED.id }).isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED.id }) + .isTrue() + assertThat(capturedEvents.any { it.id == LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED }).isTrue() + } + + companion object { + private const val KEY_ENABLE_MINUS_ONE = "pref_enable_minus_one" + private const val OVERVIEW_SUGGESTED_ACTIONS = "pref_overview_action_suggestions" + + private const val LAUNCHER_GOOGLE_APP_SWIPE_LEFT_ENABLED = 617 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt index ea52842e22..66b3b047d7 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt @@ -43,12 +43,12 @@ class LandscapePagedViewHandlerTest { if (isEnabled) { setFlagsRule.enableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } else { setFlagsRule.disableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } } @@ -62,6 +62,7 @@ class LandscapePagedViewHandlerTest { isRTL, OVERVIEW_TASK_MARGIN_PX, DIVIDER_SIZE_PX, + oneIconHiddenDueToSmallWidth = false, ) } @@ -107,14 +108,8 @@ class LandscapePagedViewHandlerTest { val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true) - // TODO(b/326377497): When started in fake seascape and rotated to landscape, - // the icon chips are in RTL and wrongly positioned at the right side of the snapshot. - // Top-Left app chip should be placed at the top left of the first snapshot, but because - // this issue, it's displayed at the top-right of the second snapshot. - // The Bottom-Right app chip is displayed at the top-right of the first snapshot because - // of this issue. - assertThat(topLeftY).isEqualTo(0) - assertThat(bottomRightY).isEqualTo(-316) + assertThat(topLeftY).isEqualTo(-316) + assertThat(bottomRightY).isEqualTo(0) } /** Test updateSplitIconsPosition */ diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt index 2bc182c02d..d455b0d1e9 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt @@ -43,12 +43,12 @@ class SeascapePagedViewHandlerTest { if (isEnabled) { setFlagsRule.enableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } else { setFlagsRule.disableFlags( Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, - Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU, ) } } @@ -62,6 +62,7 @@ class SeascapePagedViewHandlerTest { isRTL, OVERVIEW_TASK_MARGIN_PX, DIVIDER_SIZE_PX, + oneIconHiddenDueToSmallWidth = false, ) } @@ -109,12 +110,6 @@ class SeascapePagedViewHandlerTest { val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true) - // TODO(b/326377497): When started in fake seascape and rotated to landscape, - // the icon chips are in RTL and wrongly positioned at the right side of the snapshot. - // Top-Left app chip should be placed at the top left of the first snapshot, but because - // this issue, it's displayed at the top-right of the second snapshot. - // The Bottom-Right app chip is displayed at the top-right of the first snapshot because - // of this issue. assertThat(topLeftY).isEqualTo(316) assertThat(bottomRightY).isEqualTo(0) } @@ -166,7 +161,7 @@ class SeascapePagedViewHandlerTest { `when`(iconView.layoutParams).thenReturn(frameLayout) sut.updateSplitIconsPosition(iconView, expectedTranslationY, false) - assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START) + assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.END) verify(iconView).setSplitTranslationX(0f) verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) } @@ -181,7 +176,7 @@ class SeascapePagedViewHandlerTest { `when`(iconView.layoutParams).thenReturn(frameLayout) sut.updateSplitIconsPosition(iconView, expectedTranslationY, true) - assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END) + assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.START) verify(iconView).setSplitTranslationX(0f) verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/AppTimersRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/AppTimersRepositoryTest.kt new file mode 100644 index 0000000000..49ef67ce4a --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/AppTimersRepositoryTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import android.content.pm.LauncherApps +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.TestDispatcherProvider +import com.google.common.truth.Truth.assertThat +import java.time.Duration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class AppTimersRepositoryTest { + private val launcherAppsMock: LauncherApps = mock() + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val systemUnderTest = + AppTimersRepositoryImpl(launcherAppsMock, TestDispatcherProvider(testDispatcher)) + + @Test + fun getRemainingDuration_noSetLimit_returnsNull() = + testScope.runTest { + whenever(launcherAppsMock.getAppUsageLimit(PACKAGE_NAME, USER_HANDLE)).thenReturn(null) + + val remainingDuration = systemUnderTest.getRemainingDuration(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isNull() + } + + @Test + fun getRemainingDuration_limitSet_returnsUsageRemaining() = + testScope.runTest { + val totalUsageLimit = Duration.ofMinutes(20) + val usageRemaining = Duration.ofMinutes(5).plusSeconds(10) + whenever(launcherAppsMock.getAppUsageLimit(PACKAGE_NAME, USER_HANDLE)) + .thenReturn( + LauncherApps.AppUsageLimit( + totalUsageLimit.toMillis(), + usageRemaining.toMillis(), + ) + ) + + val remainingDuration = systemUnderTest.getRemainingDuration(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(usageRemaining) + } + + companion object { + private const val PACKAGE_NAME = "com.test.1" + private val USER_HANDLE = UserHandle(0) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeAppTimersRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeAppTimersRepository.kt new file mode 100644 index 0000000000..02dd9ba24d --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeAppTimersRepository.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.data + +import android.os.UserHandle +import java.time.Duration + +/** + * A fake implementation of [AppTimersRepository] that supports seeding the timer information for + * testing. + */ +class FakeAppTimersRepository : AppTimersRepository { + private val timers: MutableMap = mutableMapOf() + + override suspend fun getRemainingDuration( + packageName: String, + userHandle: UserHandle, + ): Duration? = timers[TimerKey(packageName, userHandle)] + + /** Seed timer info for an app identified by the provided [packageName] and [userHandle]. */ + fun setTimer(packageName: String, userHandle: UserHandle, remainingDuration: Duration) { + timers[TimerKey(packageName, userHandle)] = remainingDuration + } + + /** Clear timer for an app identified by the provided [packageName] and [userHandle]. */ + fun resetTimer(packageName: String, userHandle: UserHandle) { + timers.remove(TimerKey(packageName, userHandle)) + } + + private data class TimerKey(val packageName: String, val userHandle: UserHandle) +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt new file mode 100644 index 0000000000..4adf01ef34 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt @@ -0,0 +1,31 @@ +/* + * 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.quickstep.recents.data + +import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback + +class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier { + val listeners = mutableListOf() + + override fun addCallback(callback: HighResLoadingStateChangedCallback) { + listeners.add(callback) + } + + override fun removeCallback(callback: HighResLoadingStateChangedCallback) { + listeners.remove(callback) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt index eaeb513ea5..9e99a0bd62 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentTasksDataSource.kt @@ -20,10 +20,12 @@ import com.android.quickstep.util.GroupTask import java.util.function.Consumer class FakeRecentTasksDataSource : RecentTasksDataSource { - var taskList: List = listOf() + private var taskList: List = listOf() override fun getTasks(callback: Consumer>?): Int { - callback?.accept(taskList) + // Makes a copy of the GroupTask to create a new GroupTask instance and to simulate + // RecentsModel::getTasks behavior. + callback?.accept(taskList.map { it.copy() }) return 0 } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt new file mode 100644 index 0000000000..4e909035bb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsDeviceProfileRepository.kt @@ -0,0 +1,28 @@ +/* + * 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.quickstep.recents.data + +class FakeRecentsDeviceProfileRepository : RecentsDeviceProfileRepository { + private var recentsDeviceProfile = + RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false) + + override fun getRecentsDeviceProfile() = recentsDeviceProfile + + fun setRecentsDeviceProfile(newValue: RecentsDeviceProfile) { + recentsDeviceProfile = newValue + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt new file mode 100644 index 0000000000..c328672d9d --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeRecentsRotationStateRepository.kt @@ -0,0 +1,33 @@ +/* + * 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.quickstep.recents.data + +import android.view.Surface + +class FakeRecentsRotationStateRepository : RecentsRotationStateRepository { + private var recentsRotationState = + RecentsRotationState( + activityRotation = Surface.ROTATION_0, + orientationHandlerRotation = Surface.ROTATION_0 + ) + + override fun getRecentsRotationState() = recentsRotationState + + fun setRecentsRotationState(newValue: RecentsRotationState) { + recentsRotationState = newValue + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt new file mode 100644 index 0000000000..ce36715ad9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt @@ -0,0 +1,77 @@ +/* + * 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.quickstep.recents.data + +import android.graphics.drawable.Drawable +import com.android.quickstep.TaskIconCache.TaskCacheEntry +import com.android.quickstep.task.thumbnail.data.TaskIconDataSource +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class FakeTaskIconDataSource : TaskIconDataSource { + + val taskIdToDrawable: MutableMap = + (0..10).associateWith { mockCopyableDrawable() }.toMutableMap() + private val completionPrevented: MutableSet = mutableSetOf() + + /** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */ + override suspend fun getIcon(task: Task): TaskCacheEntry { + while (task.key.id in completionPrevented) { + // yield doesn't work here with an UnconfinedTestDispatcher + delay(1L) + } + return TaskCacheEntry( + taskIdToDrawable.getValue(task.key.id), + "content desc ${task.key.id}", + "title ${task.key.id}", + ) + } + + fun preventIconLoad(taskId: Int) { + completionPrevented.add(taskId) + } + + fun completeLoadingForTask(taskId: Int) { + completionPrevented.remove(taskId) + } + + fun completeLoading() { + completionPrevented.clear() + } + + companion object { + fun mockCopyableDrawable(): Drawable { + val mutableDrawable = mock() + val immutableDrawable = + mock().apply { whenever(mutate()).thenReturn(mutableDrawable) } + val constantState = + mock().apply { + whenever(newDrawable()).thenReturn(immutableDrawable) + } + return mutableDrawable.apply { whenever(this.constantState).thenReturn(constantState) } + } + } +} + +fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) { + assertThat(icon).isEqualTo(fakeTaskIconDataSource.taskIdToDrawable[key.id]) + assertThat(titleDescription).isEqualTo("content desc ${key.id}") + assertThat(title).isEqualTo("title ${key.id}") +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt index b66b7351bf..71996c7835 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt @@ -17,36 +17,46 @@ package com.android.quickstep.recents.data import android.graphics.Bitmap -import com.android.launcher3.util.CancellableTask import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData -import java.util.function.Consumer +import kotlinx.coroutines.delay import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever class FakeTaskThumbnailDataSource : TaskThumbnailDataSource { - val taskIdToBitmap: Map = (0..10).associateWith { mock() } - val taskIdToUpdatingTask: MutableMap Unit> = mutableMapOf() - var shouldLoadSynchronously: Boolean = true + val taskIdToBitmap: MutableMap = + (0..10).associateWith { mock() }.toMutableMap() + private val completionPrevented: MutableSet = mutableSetOf() + private val getThumbnailCalls = mutableMapOf() + + var highResEnabled = true /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */ - override fun updateThumbnailInBackground( - task: Task, - callback: Consumer - ): CancellableTask? { - val thumbnailData = mock() - whenever(thumbnailData.thumbnail).thenReturn(taskIdToBitmap[task.key.id]) - val wrappedCallback = { - task.thumbnail = thumbnailData - callback.accept(thumbnailData) + override suspend fun getThumbnail(task: Task): ThumbnailData { + getThumbnailCalls[task.key.id] = (getThumbnailCalls[task.key.id] ?: 0) + 1 + + while (task.key.id in completionPrevented) { + // yield doesn't work here with an UnconfinedTestDispatcher + delay(1L) } - if (shouldLoadSynchronously) { - wrappedCallback() - } else { - taskIdToUpdatingTask[task.key.id] = wrappedCallback - } - return null + return ThumbnailData( + thumbnail = taskIdToBitmap[task.key.id], + reducedResolution = !highResEnabled, + ) + } + + fun getNumberOfGetThumbnailCalls(taskId: Int): Int = getThumbnailCalls[taskId] ?: 0 + + fun preventThumbnailLoad(taskId: Int) { + completionPrevented.add(taskId) + } + + fun completeLoadingForTask(taskId: Int) { + completionPrevented.remove(taskId) + } + + fun completeLoading() { + completionPrevented.clear() } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt new file mode 100644 index 0000000000..765f0d1dfb --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskVisualsChangeNotifier.kt @@ -0,0 +1,31 @@ +/* + * 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.quickstep.recents.data + +import com.android.quickstep.util.TaskVisualsChangeListener + +class FakeTaskVisualsChangeNotifier : TaskVisualsChangeNotifier { + val listeners = mutableListOf() + + override fun addThumbnailChangeListener(listener: TaskVisualsChangeListener) { + listeners.add(listener) + } + + override fun removeThumbnailChangeListener(listener: TaskVisualsChangeListener) { + listeners.remove(listener) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt index e160627d75..c00ff15ced 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt @@ -16,25 +16,66 @@ package com.android.quickstep.recents.data +import android.graphics.drawable.Drawable +import androidx.core.util.forEach +import androidx.core.util.putAll import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update class FakeTasksRepository : RecentTasksRepository { private var thumbnailDataMap: Map = emptyMap() + private var taskIconDataMap: Map = emptyMap() private var tasks: MutableStateFlow> = MutableStateFlow(emptyList()) - private var visibleTasks: MutableStateFlow> = MutableStateFlow(emptyList()) + private var visibleTasks: MutableStateFlow>> = MutableStateFlow(mapOf()) - override fun getAllTaskData(forceRefresh: Boolean): Flow> = tasks + override fun getAllTaskData(displayId: Int, forceRefresh: Boolean): Flow> = + tasks.map { it.filter { it.key.displayId == displayId }.toList() } override fun getTaskDataById(taskId: Int): Flow = - getAllTaskData().map { taskList -> taskList.firstOrNull { it.key.id == taskId } } + combine(tasks, visibleTasks) { taskList, visibleTasks -> + val allVisibleTasks = mutableSetOf() + visibleTasks.forEach { _, value -> allVisibleTasks.addAll(value) } + taskList.filter { allVisibleTasks.contains(it.key.id) } + } + .map { taskList -> + val task = taskList.firstOrNull { it.key.id == taskId } ?: return@map null + Task(task).apply { + thumbnail = task.thumbnail + icon = task.icon + titleDescription = task.titleDescription + title = task.title + } + } - override fun setVisibleTasks(visibleTaskIdList: List) { - visibleTasks.value = visibleTaskIdList - tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } } + override fun getThumbnailById(taskId: Int): Flow = + getTaskDataById(taskId).map { it?.thumbnail } + + override fun getCurrentThumbnailById(taskId: Int): ThumbnailData? = + tasks.value.firstOrNull { it.key.id == taskId }?.thumbnail + + override fun setVisibleTasks(displayId: Int, visibleTaskIdList: Set) { + visibleTasks.update { + val newVisibleTasks = mutableMapOf>() + newVisibleTasks.putAll(it) + newVisibleTasks[displayId] = visibleTaskIdList + newVisibleTasks + } + tasks.value = + tasks.value.map { + it.apply { + thumbnail = thumbnailDataMap[it.key.id] + taskIconDataMap[it.key.id]?.let { data -> + title = data.title + titleDescription = data.titleDescription + icon = data.icon + } + } + } } fun seedTasks(tasks: List) { @@ -44,4 +85,15 @@ class FakeTasksRepository : RecentTasksRepository { fun seedThumbnailData(thumbnailDataMap: Map) { this.thumbnailDataMap = thumbnailDataMap } + + fun seedIconData(id: Int, title: String, contentDescription: String, icon: Drawable) { + val iconData = FakeIconData(icon, contentDescription, title) + this.taskIconDataMap = mapOf(id to iconData) + } + + private data class FakeIconData( + val icon: Drawable, + val titleDescription: String, + val title: String, + ) } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt new file mode 100644 index 0000000000..017f037fee --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/RecentsRotationStateRepositoryImplTest.kt @@ -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. + */ + +package com.android.quickstep.recents.data + +import android.view.Surface.ROTATION_270 +import android.view.Surface.ROTATION_90 +import com.android.quickstep.orientation.SeascapePagedViewHandler +import com.android.quickstep.util.RecentsOrientedState +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Test for [RecentsRotationStateRepositoryImpl] */ +class RecentsRotationStateRepositoryImplTest { + private val recentsOrientedState = mock() + + private val systemUnderTest = RecentsRotationStateRepositoryImpl(recentsOrientedState) + + @Test + fun orientedStateMappedCorrectly() { + whenever(recentsOrientedState.recentsActivityRotation).thenReturn(ROTATION_90) + whenever(recentsOrientedState.orientationHandler).thenReturn(SeascapePagedViewHandler()) + + assertThat(systemUnderTest.getRecentsRotationState()) + .isEqualTo( + RecentsRotationState( + activityRotation = ROTATION_90, + orientationHandlerRotation = ROTATION_270 + ) + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt new file mode 100644 index 0000000000..b91f8bd6fd --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt @@ -0,0 +1,213 @@ +/* + * 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.quickstep.recents.data + +import android.content.ComponentName +import android.content.Intent +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback +import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions + +@RunWith(AndroidJUnit4::class) +class TaskVisualsChangedDelegateTest { + private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier() + private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier() + + val systemUnderTest = + TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier) + + @Test + fun addingFirstListener_addsListenerToNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + + assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest) + assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest) + } + + @Test + fun addingAndRemovingListener_removesListenerFromNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1)) + + assertThat(taskVisualsChangeNotifier.listeners).isEmpty() + assertThat(highResLoadingStateNotifier.listeners).isEmpty() + } + + @Test + fun addingTwoAndRemovingOneListener_doesNotRemoveListenerFromNotifiers() { + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), mock()) + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 2), mock()) + systemUnderTest.unregisterTaskThumbnailChangedCallback(createTaskKey(id = 1)) + + assertThat(taskVisualsChangeNotifier.listeners.single()).isEqualTo(systemUnderTest) + assertThat(highResLoadingStateNotifier.listeners.single()).isEqualTo(systemUnderTest) + } + + @Test + fun onTaskIconChangedWithTaskId_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 1), expectedListener) + systemUnderTest.registerTaskIconChangedCallback(createTaskKey(id = 2), additionalListener) + + systemUnderTest.onTaskIconChanged(1) + + verify(expectedListener).onTaskIconChanged() + verifyNoMoreInteractions(additionalListener) + } + + @Test + fun onTaskIconChangedWithoutTaskId_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val listener = mock() + // Correct match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + expectedListener, + ) + // 1 out of 2 match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1), + listener, + ) + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2), + listener, + ) + // 0 out of 2 match + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2), + listener, + ) + + systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1)) + + verify(expectedListener).onTaskIconChanged() + verifyNoMoreInteractions(listener) + } + + @Test + fun replacedTaskIconChangedCallbacks_notCalled() { + val replacedListener = mock() + val newListener = mock() + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + replacedListener, + ) + systemUnderTest.registerTaskIconChangedCallback( + createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1), + newListener, + ) + + systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1)) + + verifyNoMoreInteractions(replacedListener) + verify(newListener).onTaskIconChanged() + } + + @Test + fun onTaskThumbnailChanged_notifiesCorrectListenerOnly() { + val expectedListener = mock() + val additionalListener = mock() + val expectedThumbnailData = ThumbnailData(snapshotId = 12345) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener, + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener, + ) + + systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData) + + verify(expectedListener).onTaskThumbnailChanged(expectedThumbnailData) + verifyNoMoreInteractions(additionalListener) + } + + @Test + fun onHighResLoadingStateChanged_toEnabled_notifiesAllListeners() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener, + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener, + ) + + systemUnderTest.onHighResLoadingStateChanged(true) + + verify(expectedListener).onHighResLoadingStateChanged(true) + verify(additionalListener).onHighResLoadingStateChanged(true) + } + + @Test + fun onHighResLoadingStateChanged_toDisabled_notifiesAllListeners() { + val expectedListener = mock() + val additionalListener = mock() + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + expectedListener, + ) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 2), + additionalListener, + ) + + systemUnderTest.onHighResLoadingStateChanged(false) + + verify(expectedListener).onHighResLoadingStateChanged(false) + verify(additionalListener).onHighResLoadingStateChanged(false) + } + + @Test + fun replacedTaskThumbnailChangedCallbacks_notCalled() { + val replacedListener1 = mock() + val newListener1 = mock() + val expectedThumbnailData = ThumbnailData(snapshotId = 12345) + systemUnderTest.registerTaskThumbnailChangedCallback( + createTaskKey(id = 1), + replacedListener1, + ) + systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1) + + systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData) + + verifyNoMoreInteractions(replacedListener1) + verify(newListener1).onTaskThumbnailChanged(expectedThumbnailData) + } + + private fun createTaskKey(id: Int = 1, pkg: String = PACKAGE_NAME, userId: Int = 1) = + TaskKey(id, 0, Intent().setPackage(pkg), ComponentName("", ""), userId, 0) + + private companion object { + const val PACKAGE_NAME = "com.test.test" + const val ALTERNATIVE_PACKAGE_NAME = "com.test.test2" + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt index c28a85a8f8..e7f1d18e85 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt @@ -18,133 +18,603 @@ package com.android.quickstep.recents.data import android.content.ComponentName import android.content.Intent -import com.android.quickstep.TaskIconCache +import android.graphics.Bitmap +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.TestDispatcherProvider import com.android.quickstep.util.DesktopTask -import com.android.quickstep.util.GroupTask +import com.android.quickstep.util.SingleTask +import com.android.quickstep.util.SplitTask import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) class TasksRepositoryTest { private val tasks = (0..5).map(::createTaskWithId) + private val secondaryTasks = (6..11).map(::createTaskWithIdForSecondaryDisplay) private val defaultTaskList = listOf( - GroupTask(tasks[0]), - GroupTask(tasks[1], tasks[2], null), - DesktopTask(tasks.subList(3, 6)) + SingleTask(tasks[0]), + SplitTask( + tasks[1], + tasks[2], + SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ 1, + /* rightBottomTaskId = */ 2, + /* snapPosition = */ SNAP_TO_2_50_50, + ), + ), + DesktopTask(deskId = 0, DEFAULT_DISPLAY, tasks.subList(3, 6)), + ) + private val secondaryTaskList = + listOf( + SingleTask(secondaryTasks[0]), + SplitTask( + secondaryTasks[1], + secondaryTasks[2], + SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ 7, + /* rightBottomTaskId = */ 8, + /* snapPosition = */ SNAP_TO_2_50_50, + ), + ), + DesktopTask(deskId = 1, SECONDARY_DISPLAY, secondaryTasks.subList(3, 6)), ) private val recentsModel = FakeRecentTasksDataSource() private val taskThumbnailDataSource = FakeTaskThumbnailDataSource() - private val taskIconCache = mock() + private val taskIconDataSource = FakeTaskIconDataSource() + private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier() + private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier() + private val taskVisualsChangedDelegate = + spy(TaskVisualsChangedDelegateImpl(taskVisualsChangeNotifier, highResLoadingStateNotifier)) + private val dispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(dispatcher) private val systemUnderTest = - TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache) + TasksRepository( + recentsModel, + taskThumbnailDataSource, + taskIconDataSource, + taskVisualsChangedDelegate, + testScope.backgroundScope, + TestDispatcherProvider(dispatcher), + ) - @Test - fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest { - recentsModel.seedTasks(defaultTaskList) - - assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks) + @Before + fun cleanupDataSources() { + taskThumbnailDataSource.completeLoading() + taskIconDataSource.completeLoading() } @Test - fun getTaskDataByIdReturnsSpecificTask() = runTest { - recentsModel.seedTasks(defaultTaskList) - systemUnderTest.getAllTaskData(forceRefresh = true) - - assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2]) - } - - @Test - fun setVisibleTasksPopulatesThumbnails() = runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // .drop(1) to ignore initial null content before from thumbnail was loaded. - assertThat(systemUnderTest.getTaskDataById(1).drop(1).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap1) - assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - } - - @Test - fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // .drop(1) to ignore initial null content before from thumbnail was loaded. - assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - - // Prevent new loading of Bitmaps - taskThumbnailDataSource.shouldLoadSynchronously = false - systemUnderTest.setVisibleTasks(listOf(2, 3)) - - assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - } - - @Test - fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest { - recentsModel.seedTasks(defaultTaskList) - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - systemUnderTest.getAllTaskData(forceRefresh = true) - - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // .drop(1) to ignore initial null content before from thumbnail was loaded. - assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail) - .isEqualTo(bitmap2) - - // Prevent new loading of Bitmaps - taskThumbnailDataSource.shouldLoadSynchronously = false - systemUnderTest.setVisibleTasks(listOf(0, 1)) - - assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull() - } - - @Test - fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = runTest { - // Setup fakes - recentsModel.seedTasks(defaultTaskList) - val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] - taskThumbnailDataSource.shouldLoadSynchronously = false - - // Setup TasksRepository - systemUnderTest.getAllTaskData(forceRefresh = true) - systemUnderTest.setVisibleTasks(listOf(1, 2)) - - // Assert there is no bitmap in first emission - val taskFlow = systemUnderTest.getTaskDataById(2) - val taskFlowValuesList = mutableListOf() - backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - taskFlow.toList(taskFlowValuesList) + fun getAllTaskDataReturnsFlattenedListOfTasks() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + assertThat(systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true).first()) + .isEqualTo(tasks) } - assertThat(taskFlowValuesList[0]!!.thumbnail).isNull() - // Simulate bitmap loading after first emission - taskThumbnailDataSource.taskIdToUpdatingTask.getValue(2).invoke() + @Test + fun getTaskDataByIdReturnsSpecificTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) - // Check for second emission - assertThat(taskFlowValuesList[1]!!.thumbnail!!.thumbnail).isEqualTo(bitmap2) - } + assertThat(systemUnderTest.getTaskDataById(2).first()).isEqualTo(tasks[2]) + } + + @Test + fun getThumbnailByIdReturnsNullWithNoLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + assertThat(systemUnderTest.getThumbnailById(1).first()).isNull() + } + + @Test + fun getCurrentThumbnailByIdReturnsNullWithNoLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + assertThat(systemUnderTest.getCurrentThumbnailById(1)).isNull() + } + + @Test + fun getThumbnailByIdReturnsThumbnailWithLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + } + + @Test + fun whenThumbnailIsLoaded_getAllTaskData_usesPreviousLoadedThumbnailAndIcon() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + } + + @Test + fun getAllTaskData_copiesPreviouslyLoadedImagesForTasksStillPresent() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(0, 1)) + + // Seed new task with same id + val newTask0 = SingleTask(createTaskWithId(0)) + val newSeededTasks = + defaultTaskList.map { + if (it is SingleTask && it.task.key.id == 0) newTask0 else it + } + recentsModel.seedTasks(newSeededTasks) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + // Assert no additional loads, assert images present + assertThat(systemUnderTest.getThumbnailById(0).first()?.thumbnail) + .isEqualTo(taskThumbnailDataSource.taskIdToBitmap[0]) + assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(0)).isEqualTo(1) + } + + @Test + fun getAllTaskData_clearsPreviouslyLoadedImagesForRemovedTasks() = + testScope.runTest { + // Setup data + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + // Load images for task 1 + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + assertThat(systemUnderTest.getThumbnailById(1).first()!!.thumbnail).isEqualTo(bitmap1) + + // Remove task 1 from "all data" + recentsModel.seedTasks( + defaultTaskList.filterNot { groupTask -> groupTask.tasks.any { it.key.id == 1 } } + ) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + // Assert task 1 was fully removed + assertThat(systemUnderTest.getThumbnailById(1).first()?.thumbnail).isNull() + verify(taskVisualsChangedDelegate).unregisterTaskThumbnailChangedCallback(tasks[1].key) + } + + @Test + fun getCurrentThumbnailByIdReturnsThumbnailWithLoadedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + + assertThat(systemUnderTest.getCurrentThumbnailById(1)?.thumbnail).isEqualTo(bitmap1) + } + + @Test + fun setVisibleTasksPopulatesThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1, 2)) + + assertThat(systemUnderTest.getTaskDataById(1).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap1) + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun setVisibleTasksPopulatesIcons() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1, 2)) + + systemUnderTest + .getTaskDataById(1) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + systemUnderTest + .getTaskDataById(2) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + } + + @Test + fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1, 2)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + + // Prevent new loading of Bitmaps + taskThumbnailDataSource.preventThumbnailLoad(2) + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(2, 3)) + + assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail) + .isEqualTo(bitmap2) + } + + @Test + fun changingVisibleTasksContainsAlreadyPopulatedIcons() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1, 2)) + + systemUnderTest + .getTaskDataById(2) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + + // Prevent new loading of Drawables + taskIconDataSource.preventIconLoad(2) + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(2, 3)) + + systemUnderTest + .getTaskDataById(2) + .first()!! + .assertHasIconDataFromSource(taskIconDataSource) + } + + @Test + fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1, 2)) + + val task2 = systemUnderTest.getTaskDataById(2).first()!! + assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2) + task2.assertHasIconDataFromSource(taskIconDataSource) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(0, 1)) + + val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!! + assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull() + assertThat(task2AfterVisibleTasksChanged.icon).isNull() + assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull() + assertThat(task2AfterVisibleTasksChanged.title).isNull() + } + + @Test + fun retrievedThumbnailsCauseEmissionOnTaskDataFlow() = + testScope.runTest { + // Setup fakes + recentsModel.seedTasks(defaultTaskList) + val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2] + + // Setup TasksRepository + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + val task2DataFlow = systemUnderTest.getTaskDataById(2) + val task2BitmapValues = mutableListOf() + testScope.backgroundScope.launch { + task2DataFlow.map { it?.thumbnail?.thumbnail }.toList(task2BitmapValues) + } + + // Check for first emission + assertThat(task2BitmapValues.single()).isNull() + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(2)) + // Check for second emission + assertThat(task2BitmapValues).isEqualTo(listOf(null, bitmap2)) + } + + @Test + fun onTaskThumbnailChanged_setsNewThumbnailDataOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + + val expectedThumbnailData = createThumbnailData() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues) + } + taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData) + + assertThat(task1ThumbnailValues.first()!!.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData) + } + + @Test + fun onHighResLoadingStateChanged_highResReplacesLowResThumbnail() = + testScope.runTest { + taskThumbnailDataSource.highResEnabled = false + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + + val expectedBitmap = mock() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues) + } + + taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap + taskThumbnailDataSource.highResEnabled = true + taskVisualsChangedDelegate.onHighResLoadingStateChanged(true) + + val firstThumbnailValue = task1ThumbnailValues.first()!! + assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(firstThumbnailValue.reducedResolution).isTrue() + + val lastThumbnailValue = task1ThumbnailValues.last()!! + assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedBitmap) + assertThat(lastThumbnailValue.reducedResolution).isFalse() + } + + @Test + fun onHighResLoadingStateChanged_invisibleTaskIgnored() = + testScope.runTest { + taskThumbnailDataSource.highResEnabled = false + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + + val invisibleTaskId = 2 + val taskDataFlow = systemUnderTest.getTaskDataById(invisibleTaskId) + + val task2ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task2ThumbnailValues) + } + + taskThumbnailDataSource.highResEnabled = true + taskVisualsChangedDelegate.onHighResLoadingStateChanged(true) + + assertThat(task2ThumbnailValues.filterNotNull()).isEmpty() + assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(2)).isEqualTo(0) + } + + @Test + fun onHighResLoadingStateChanged_lowResDoesNotReplaceHighResThumbnail() = + testScope.runTest { + taskThumbnailDataSource.highResEnabled = true + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + + val expectedBitmap = mock() + val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1ThumbnailValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues) + } + + taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap + taskThumbnailDataSource.highResEnabled = false + taskVisualsChangedDelegate.onHighResLoadingStateChanged(false) + + val firstThumbnailValue = task1ThumbnailValues.first()!! + assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(firstThumbnailValue.reducedResolution).isFalse() + + val lastThumbnailValue = task1ThumbnailValues.last()!! + assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap) + assertThat(lastThumbnailValue.reducedResolution).isFalse() + } + + @Test + fun onTaskIconChanged_setsNewIconOnTask() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + + val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable() + val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1] + val taskDataFlow = systemUnderTest.getTaskDataById(1) + + val task1IconValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.icon }.toList(task1IconValues) + } + taskIconDataSource.taskIdToDrawable[1] = expectedIcon + taskVisualsChangedDelegate.onTaskIconChanged(1) + + assertThat(task1IconValues.first()).isEqualTo(expectedPreviousIcon) + assertThat(task1IconValues.last()).isEqualTo(expectedIcon) + } + + @Test + fun setVisibleTasks_multipleTimesWithDifferentTasks_reusesThumbnailRequests() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + + val taskDataFlow = systemUnderTest.getTaskDataById(1) + val task1IconValues = mutableListOf() + testScope.backgroundScope.launch { + taskDataFlow.map { it?.icon }.toList(task1IconValues) + } + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1) + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1, 2)) + assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(1)).isEqualTo(1) + } + + @Test + fun getTaskData_forSecondaryDisplay() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList + secondaryTaskList) + + assertThat(systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true).first()) + .isEqualTo(tasks) + assertThat( + systemUnderTest.getAllTaskData(SECONDARY_DISPLAY, forceRefresh = true).first() + ) + .isEqualTo(secondaryTasks) + } + + @Test + fun setVisibleTasks_secondaryDisplayConnected() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList + secondaryTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + systemUnderTest.getAllTaskData(SECONDARY_DISPLAY, forceRefresh = true) + val bitmap1 = taskThumbnailDataSource.taskIdToBitmap[1] + val bitmap6 = taskThumbnailDataSource.taskIdToBitmap[6] + + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + systemUnderTest.setVisibleTasks(SECONDARY_DISPLAY, setOf(6)) + + val actualThumbnails = + (0..11).map { systemUnderTest.getThumbnailById(it).first()?.thumbnail }.toList() + val expectedThumbnails = + arrayOfNulls(12) + .apply { + set(1, bitmap1) + set(6, bitmap6) + } + .toList() + assertThat(actualThumbnails).isEqualTo(expectedThumbnails) + } + + @Test + fun setVisibleTasks_displayDisconnectedBeforeImageReturns_doesNotPopulateThumbnailOrIcon() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList + secondaryTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + systemUnderTest.getAllTaskData(SECONDARY_DISPLAY, forceRefresh = true) + taskThumbnailDataSource.preventThumbnailLoad(6) + taskIconDataSource.preventIconLoad(6) + + val task6DataFlow = systemUnderTest.getTaskDataById(6) + val task6Values = mutableListOf>() + testScope.backgroundScope.launch { + task6DataFlow.map { Pair(it?.thumbnail?.thumbnail, it?.icon) }.toList(task6Values) + } + + launch { systemUnderTest.setVisibleTasks(SECONDARY_DISPLAY, setOf(6)) } + // Check we prevented the resources from loading + assertThat(task6Values.distinct()).isEqualTo(listOf(Pair(null, null))) + // Display disconnects + launch { systemUnderTest.setVisibleTasks(SECONDARY_DISPLAY, emptySet()) } + taskThumbnailDataSource.completeLoadingForTask(6) + taskIconDataSource.completeLoadingForTask(6) + testScope.advanceUntilIdle() + // Still should not be loaded + assertThat(task6Values.distinct()).isEqualTo(listOf(Pair(null, null))) + } + + @Test + fun onDisplayRemoved_removesAllDataForDisplay() = + testScope.runTest { + recentsModel.seedTasks(defaultTaskList + secondaryTaskList) + systemUnderTest.getAllTaskData(DEFAULT_DISPLAY, forceRefresh = true) + systemUnderTest.getAllTaskData(SECONDARY_DISPLAY, forceRefresh = true) + systemUnderTest.setVisibleTasks(DEFAULT_DISPLAY, setOf(1)) + systemUnderTest.setVisibleTasks(SECONDARY_DISPLAY, setOf(6)) + + recentsModel.seedTasks(defaultTaskList) + systemUnderTest.setVisibleTasks(SECONDARY_DISPLAY, emptySet()) + + for (t in 6..11) { + assertThat(systemUnderTest.getThumbnailById(t).first()?.thumbnail).isNull() + assertThat(systemUnderTest.getTaskDataById(t).first()?.icon).isNull() + } + } private fun createTaskWithId(taskId: Int) = Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)) + + private fun createTaskWithIdForSecondaryDisplay(taskId: Int) = + Task( + Task.TaskKey( + taskId, + 0, + Intent(), + ComponentName("", ""), + 0, + 2000, + SECONDARY_DISPLAY, + null, + 0, + false, + false, + ) + ) + + private fun createThumbnailData(): ThumbnailData { + val bitmap = mock() + whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH) + whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT) + + return ThumbnailData(thumbnail = bitmap) + } + + companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + const val SECONDARY_DISPLAY = 1 + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetRemainingAppTimerDurationUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetRemainingAppTimerDurationUseCaseTest.kt new file mode 100644 index 0000000000..f6ab1b983c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetRemainingAppTimerDurationUseCaseTest.kt @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.data.FakeAppTimersRepository +import com.google.common.truth.Truth.assertThat +import java.time.Duration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [GetRemainingAppTimerDurationUseCase]. */ +@RunWith(AndroidJUnit4::class) +class GetRemainingAppTimerDurationUseCaseTest { + private val appTimersRepository = FakeAppTimersRepository() + + @OptIn(ExperimentalCoroutinesApi::class) + private val testScope = TestScope(UnconfinedTestDispatcher()) + + private val systemUnderTest = GetRemainingAppTimerDurationUseCase(appTimersRepository) + + @Test + fun noSetLimit_returnsNull() = + testScope.runTest { + appTimersRepository.resetTimer(PACKAGE_NAME, USER_HANDLE) + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isNull() + } + + @Test + fun lessThanMinuteRemaining_aMilliSecondLess_noRounding() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(1).minusMillis(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(usageRemaining) + } + + @Test + fun lessThanMinuteRemaining_aSecondLess_noRounding() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(1).minusSeconds(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(usageRemaining) + } + + @Test + fun aWholeMinuteRemaining_returnsAMinute() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(usageRemaining) + } + + @Test + fun littleOverAMinuteRemaining_aMilliSecondMore_roundsToTwoMinutes() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(1).plusMillis(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(2)) + } + + @Test + fun littleOverAMinuteRemaining_aSecondMore_roundsToTwoMinutes() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(1).plusSeconds(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(2)) + } + + @Test + fun littleUnderTwoMinuteRemaining_aMilliSecondLess_roundsToTwoMinutes() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(2).minusMillis(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(2)) + } + + @Test + fun littleUnderTwoMinuteRemaining_aSecondLess_roundsToTwoMinutes() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(2).minusSeconds(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(2)) + } + + @Test + fun severalMinutesAndLittleOverRemaining_aMilliSecondMore_returnsRoundedMinutes() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(5).plusMillis(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(6)) + } + + @Test + fun severalMinutesAndLittleOverRemaining_aSecondMore_returnsRoundedMinutes() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(5).plusSeconds(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(6)) + } + + @Test + fun multipleWholeMinutesRemaining_noRounding() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(5) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(5)) + } + + @Test + fun multipleHoursAndASecondOver_roundsSecondToAMinute() = + testScope.runTest { + val usageRemaining = Duration.ofHours(5).plusSeconds(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofHours(5).plusMinutes(1)) + } + + @Test + fun multipleHoursAndAMilliSecondOver_roundsMillisecondToAMinute() = + testScope.runTest { + val usageRemaining = Duration.ofHours(5).plusMillis(1) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofHours(5).plusMinutes(1)) + } + + @Test + fun returnsCorrectRemainingTimeOnEachInvocation() = + testScope.runTest { + val usageRemaining = Duration.ofMinutes(5).plusSeconds(10) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemaining) + + val remainingDuration = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(remainingDuration).isEqualTo(Duration.ofMinutes(6)) + + appTimersRepository.resetTimer(PACKAGE_NAME, USER_HANDLE) + val newRemainingTime = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + + assertThat(newRemainingTime).isNull() + } + + @Test + fun differentApps_returnsCorrectRemainingTime() = + testScope.runTest { + val usageRemainingAppOne = Duration.ofMinutes(5) + val usageRemainingAppTwo = Duration.ofMinutes(2) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemainingAppOne) + appTimersRepository.setTimer(PACKAGE_NAME_TWO, USER_HANDLE, usageRemainingAppTwo) + + val remainingDurationAppOne = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + val remainingDurationAppTwo = systemUnderTest.invoke(PACKAGE_NAME_TWO, USER_HANDLE) + + assertThat(remainingDurationAppOne).isEqualTo(usageRemainingAppOne) + assertThat(remainingDurationAppTwo).isEqualTo(usageRemainingAppTwo) + } + + @Test + fun appInMultipleUsers_returnsCorrectRemainingTime() = + testScope.runTest { + val usageRemainingUserOne = Duration.ofMinutes(5) + val usageRemainingUserTwo = Duration.ofMinutes(2) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE, usageRemainingUserOne) + appTimersRepository.setTimer(PACKAGE_NAME, USER_HANDLE_TWO, usageRemainingUserTwo) + + val remainingDurationUserOne = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE) + val remainingDurationUserTwo = systemUnderTest.invoke(PACKAGE_NAME, USER_HANDLE_TWO) + + assertThat(remainingDurationUserOne).isEqualTo(usageRemainingUserOne) + assertThat(remainingDurationUserTwo).isEqualTo(usageRemainingUserTwo) + } + + companion object { + private const val PACKAGE_NAME = "com.test.1" + private val USER_HANDLE = UserHandle(0) + + private const val PACKAGE_NAME_TWO = "com.test.2" + private val USER_HANDLE_TWO = UserHandle(2) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCaseTest.kt new file mode 100644 index 0000000000..d3842562d1 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetSysUiStatusNavFlagsUseCaseTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV +import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class GetSysUiStatusNavFlagsUseCaseTest { + private val sut: GetSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase() + + @Test + fun onLightStatusBarAppearance_returns_LightTheme() { + val thumbnailData = ThumbnailData(appearance = APPEARANCE_LIGHT_STATUS_BARS) + val flag = sut.invoke(thumbnailData) // 6 + flag.assertContainsFlag(FLAG_LIGHT_STATUS) + flag.assertContainsFlag(FLAG_DARK_NAV) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + } + + @Test + fun onLightNavBarsAppearance_returns_LightTheme() { + val thumbnailData = ThumbnailData(appearance = APPEARANCE_LIGHT_NAVIGATION_BARS) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_DARK_STATUS) + flag.assertContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + @Test + fun onLightStatusBarAndNavBarAppearance_returns_LightTheme() { + val thumbnailData = + ThumbnailData( + appearance = APPEARANCE_LIGHT_STATUS_BARS or APPEARANCE_LIGHT_NAVIGATION_BARS + ) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_LIGHT_NAV) + flag.assertContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + @Test + fun onLightAppearance_returns_LightTheme() { + val thumbnailData = + ThumbnailData( + appearance = + APPEARANCE_LIGHT_CAPTION_BARS or + APPEARANCE_LIGHT_STATUS_BARS or + APPEARANCE_LIGHT_NAVIGATION_BARS + ) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_LIGHT_NAV) + flag.assertContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + @Test + fun onDarkAppearance_returns_DarkTheme() { + val thumbnailData = ThumbnailData(appearance = 0) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_DARK_STATUS) + flag.assertContainsFlag(FLAG_DARK_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + } + + @Test + fun onUnrelatedDarkAppearance_returns_DarkTheme() { + val thumbnailData = ThumbnailData(appearance = 1) + val flag = sut.invoke(thumbnailData) + flag.assertContainsFlag(FLAG_DARK_STATUS) + flag.assertContainsFlag(FLAG_DARK_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + } + + @Test + fun whenThumbnailIsNull_returns_default() { + val flag = sut.invoke(null) + flag.assertDoesNotContainsFlag(FLAG_DARK_STATUS) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_NAV) + flag.assertDoesNotContainsFlag(FLAG_LIGHT_STATUS) + flag.assertDoesNotContainsFlag(FLAG_DARK_NAV) + } + + private fun Int.assertContainsFlag(flag: Int) { + assertThat(this and flag).isNotEqualTo(0) + } + + private fun Int.assertDoesNotContainsFlag(flag: Int) { + assertThat(this and flag).isEqualTo(0) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt new file mode 100644 index 0000000000..d29183f62f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetTaskUseCaseTest.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.Flags +import com.android.quickstep.recents.data.FakeAppTimersRepository +import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.quickstep.recents.domain.model.TaskModel +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import java.time.Duration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyString +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.kotlin.any + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class GetTaskUseCaseTest { + @get:Rule val setFlagsRule = SetFlagsRule() + + private val unconfinedTestDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(unconfinedTestDispatcher) + + private val tasksRepository = FakeTasksRepository() + private val timersRepository = FakeAppTimersRepository() + private val getRemainingAppTimerDurationUseCase = + spy(GetRemainingAppTimerDurationUseCase(timersRepository)) + private val sut = + GetTaskUseCase( + tasksRepository = tasksRepository, + getRemainingAppTimerDurationUseCase = getRemainingAppTimerDurationUseCase, + ) + + @Before + fun setUp() { + tasksRepository.seedTasks(listOf(TASK_1)) + timersRepository.setTimer(PACKAGE_1, UserHandle(USER_1), REMAINING_APP_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_REFACTOR_DIGITAL_WELLBEING_TOAST) + fun taskNotSeeded_returnsNull() = + testScope.runTest { + val result = sut.invoke(NOT_FOUND_TASK_ID).firstOrNull() + + assertThat(result).isNull() + verify(getRemainingAppTimerDurationUseCase, times(0)) + .invoke(anyString(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_REFACTOR_DIGITAL_WELLBEING_TOAST) + fun taskNotVisible_returnsNull() = + testScope.runTest { + val result = sut.invoke(TASK_1_ID).firstOrNull() + + assertThat(result).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_REFACTOR_DIGITAL_WELLBEING_TOAST) + fun taskVisible_returnsData() = + testScope.runTest { + tasksRepository.setVisibleTasks(DEFAULT_DISPLAY, setOf(TASK_1_ID)) + + val result = sut.invoke(TASK_1_ID).firstOrNull() + + assertThat(result) + .isEqualTo( + TaskModel( + id = TASK_1_ID, + packageName = PACKAGE_1, + title = "Title $TASK_1_ID", + titleDescription = "Content Description $TASK_1_ID", + icon = TASK_1_ICON, + thumbnail = null, + backgroundColor = Color.BLACK, + isLocked = false, + isMinimized = false, + remainingAppDuration = ROUNDED_REMAINING_APP_DURATION, + ) + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_REFACTOR_DIGITAL_WELLBEING_TOAST) + fun taskVisible_noAppTimer_returnsDataWithoutTimer() = + testScope.runTest { + tasksRepository.setVisibleTasks(DEFAULT_DISPLAY, setOf(TASK_1_ID)) + timersRepository.resetTimer(PACKAGE_1, UserHandle(USER_1)) + + val result = sut.invoke(TASK_1_ID).firstOrNull() + + assertThat(result) + .isEqualTo( + TaskModel( + id = TASK_1_ID, + packageName = PACKAGE_1, + title = "Title $TASK_1_ID", + titleDescription = "Content Description $TASK_1_ID", + icon = TASK_1_ICON, + thumbnail = null, + backgroundColor = Color.BLACK, + isLocked = false, + isMinimized = false, + remainingAppDuration = null, + ) + ) + verify(getRemainingAppTimerDurationUseCase, times(1)) + .invoke(anyString(), any()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_REFACTOR_DIGITAL_WELLBEING_TOAST) + fun taskVisible_dwbFlagOff_doesNotFetchTimer() = + testScope.runTest { + tasksRepository.setVisibleTasks(DEFAULT_DISPLAY, setOf(TASK_1_ID)) + + val result = sut.invoke(TASK_1_ID).firstOrNull() + + assertThat(result) + .isEqualTo( + TaskModel( + id = TASK_1_ID, + packageName = PACKAGE_1, + title = "Title $TASK_1_ID", + titleDescription = "Content Description $TASK_1_ID", + icon = TASK_1_ICON, + thumbnail = null, + backgroundColor = Color.BLACK, + isLocked = false, + isMinimized = false, + remainingAppDuration = null, + ) + ) + verify(getRemainingAppTimerDurationUseCase, times(0)) + .invoke(anyString(), any()) + } + + private companion object { + const val NOT_FOUND_TASK_ID = 404 + private const val TASK_1_ID = 1 + private const val PACKAGE_1 = "com.test.1" + private const val USER_1 = 1 + private val TASK_1_ICON = ShapeDrawable() + private val REMAINING_APP_DURATION = Duration.ofHours(2).plusMillis(10) + private val ROUNDED_REMAINING_APP_DURATION = Duration.ofHours(2).plusMinutes(1) + private val TASK_1 = + Task( + Task.TaskKey( + /* id = */ TASK_1_ID, + /* windowingMode = */ 0, + /* intent = */ Intent(), + /* sourceComponent = */ ComponentName("", ""), + /* userId = */ USER_1, + /* lastActiveTime = */ 2000, + ) + ) + .apply { + title = "Title 1" + titleDescription = "Content Description 1" + colorBackground = Color.BLACK + icon = TASK_1_ICON + thumbnail = null + isLocked = false + isMinimized = false + topActivity = ComponentName(PACKAGE_1, "SomeClass") + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt new file mode 100644 index 0000000000..7646e69117 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.Rect +import android.view.Surface.ROTATION_90 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper.PreviewPositionHelperFactory +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Test for [GetThumbnailPositionUseCase] */ +@RunWith(AndroidJUnit4::class) +class GetThumbnailPositionUseCaseTest { + private val deviceProfileRepository = FakeRecentsDeviceProfileRepository() + private val rotationStateRepository = FakeRecentsRotationStateRepository() + private val previewPositionHelperFactoryMock = mock() + private val previewPositionHelper = mock() + + private val systemUnderTest = + GetThumbnailPositionUseCase( + deviceProfileRepository = deviceProfileRepository, + rotationStateRepository = rotationStateRepository, + previewPositionHelperFactory = previewPositionHelperFactoryMock, + ) + + @Before + fun setUp() { + whenever(previewPositionHelperFactoryMock.create()).thenReturn(previewPositionHelper) + } + + @Test + fun nullThumbnailData_returnsIdentityMatrix() = runTest { + val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false) + val result = systemUnderTest.invoke(null, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun withoutThumbnail_returnsIdentityMatrix() = runTest { + val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false) + val result = + systemUnderTest.invoke(ThumbnailData(), CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest { + val isLargeScreen = true + deviceProfileRepository.setRecentsDeviceProfile( + deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen) + ) + val activityRotation = ROTATION_90 + rotationStateRepository.setRecentsRotationState( + rotationStateRepository + .getRecentsRotationState() + .copy(activityRotation = activityRotation) + ) + val isRtl = true + val isRotated = true + + whenever(previewPositionHelper.matrix).thenReturn(MATRIX) + whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated) + + val result = systemUnderTest.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl) + val expectedResult = ThumbnailPosition(MATRIX, isRotated) + assertThat(result).isEqualTo(expectedResult) + + verify(previewPositionHelper) + .updateThumbnailMatrix( + Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), + THUMBNAIL_DATA, + CANVAS_WIDTH, + CANVAS_HEIGHT, + isLargeScreen, + activityRotation, + isRtl, + ) + } + + @Test + fun multipleInvocations_usesPreviewPositionHelperFactoryEachTime() = runTest { + whenever(previewPositionHelper.matrix).thenReturn(MATRIX) + + val sut = + GetThumbnailPositionUseCase( + deviceProfileRepository = deviceProfileRepository, + rotationStateRepository = rotationStateRepository, + previewPositionHelperFactory = previewPositionHelperFactoryMock, + ) + verify(previewPositionHelperFactoryMock, times(0)).create() + + sut.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, /* isRtl= */ true) + sut.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, /* isRtl= */ false) + + // Each invocation of use case should use a fresh position helper acquired by the factory. + verify(previewPositionHelperFactoryMock, times(2)).create() + } + + private companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + const val CANVAS_WIDTH = 300 + const val CANVAS_HEIGHT = 600 + val MATRIX = + Matrix().apply { + setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f)) + } + + val THUMBNAIL_DATA = + ThumbnailData( + thumbnail = + mock().apply { + whenever(width).thenReturn(THUMBNAIL_WIDTH) + whenever(height).thenReturn(THUMBNAIL_HEIGHT) + } + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt new file mode 100644 index 0000000000..e8bca937b2 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Bitmap +import android.view.Surface +import android.view.Surface.ROTATION_90 +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class IsThumbnailValidUseCaseTest { + private val recentsRotationStateRepository = FakeRecentsRotationStateRepository() + private val systemUnderTest = IsThumbnailValidUseCase(recentsRotationStateRepository) + + @Test + fun withNullThumbnail_returnsInvalid() = runTest { + val isThumbnailValid = systemUnderTest(thumbnailData = null, viewWidth = 0, viewHeight = 0) + assertThat(isThumbnailValid).isEqualTo(false) + } + + @Test + fun sameAspectRatio_sameRotation_returnsValid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(), + viewWidth = THUMBNAIL_WIDTH * 2, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(true) + } + + @Test + fun differentAspectRatio_sameRotation_returnsInvalid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(), + viewWidth = THUMBNAIL_WIDTH, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(false) + } + + @Test + fun sameAspectRatio_differentRotation_returnsInvalid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(rotation = ROTATION_90), + viewWidth = THUMBNAIL_WIDTH * 2, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(false) + } + + @Test + fun differentAspectRatio_differentRotation_returnsInvalid() = runTest { + val isThumbnailValid = + systemUnderTest.invoke( + thumbnailData = createThumbnailData(rotation = ROTATION_90), + viewWidth = THUMBNAIL_WIDTH, + viewHeight = THUMBNAIL_HEIGHT * 2, + ) + assertThat(isThumbnailValid).isEqualTo(false) + } + + private fun createThumbnailData( + rotation: Int = Surface.ROTATION_0, + width: Int = THUMBNAIL_WIDTH, + height: Int = THUMBNAIL_HEIGHT, + ): ThumbnailData { + val bitmap = mock() + whenever(bitmap.width).thenReturn(width) + whenever(bitmap.height).thenReturn(height) + return ThumbnailData(thumbnail = bitmap, rotation = rotation) + } + + companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCaseTest.kt new file mode 100644 index 0000000000..f2038b28d9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCaseTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.domain.usecase + +import android.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.quickstep.recents.domain.model.DesktopLayoutConfig +import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** Test for [OrganizeDesktopTasksUseCase] */ +@RunWith(AndroidJUnit4::class) +class OrganizeDesktopTasksUseCaseTest { + + private val useCase: OrganizeDesktopTasksUseCase = OrganizeDesktopTasksUseCase() + private val testLayoutConfig: DesktopLayoutConfig = + DesktopLayoutConfig( + topBottomMarginOneRow = 20, + topMarginMultiRows = 20, + bottomMarginMultiRows = 20, + leftRightMarginOneRow = 20, + leftRightMarginMultiRows = 20, + horizontalPaddingBetweenTasks = 10, + verticalPaddingBetweenTasks = 10, + ) + + @Test + fun test_emptyTaskBounds_returnsEmptyList() { + val desktopBounds = Rect(0, 0, 1000, 2000) + val taskBounds = emptyList() + + val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig) + + assertThat(result).isEmpty() + } + + @Test + fun test_emptyDesktopBounds_returnsEmptyList() { + val desktopBounds = Rect(0, 0, 0, 0) + val taskBounds = listOf(DesktopTaskBoundsData(1, Rect(0, 0, 100, 100))) + + val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig) + + assertThat(result).isEmpty() + } + + @Test + fun test_filtersOutTasksWithEmptyBounds() { + val desktopBounds = Rect(0, 0, 1000, 2000) + val taskBounds = + listOf( + DesktopTaskBoundsData(1, Rect(0, 0, 100, 100)), + DesktopTaskBoundsData(2, Rect()), // Empty bounds + DesktopTaskBoundsData(3, Rect(0, 0, 50, 50)), + ) + + val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig) + assertThat(result) + .isEqualTo( + listOf( + DesktopTaskBoundsData(1, Rect(20, 34, 980, 995)), + DesktopTaskBoundsData(3, Rect(20, 1005, 980, 1966)), + ) + ) + } + + @Test + fun test_singleTask_isCenteredAndScaled() { + val desktopBounds = Rect(0, 0, 1000, 2000) + val originalAppRect = Rect(0, 0, 800, 1200) + val taskBounds = listOf(DesktopTaskBoundsData(1, originalAppRect)) + + val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig) + + assertThat(result).hasSize(1) + val resultBounds = result[0].bounds + assertThat(resultBounds.width()).isGreaterThan(0) + assertThat(resultBounds.height()).isGreaterThan(0) + + // Check aspect ratio is roughly preserved + val originalAspectRatio = originalAppRect.width().toFloat() / originalAppRect.height() + val resultAspectRatio = resultBounds.width().toFloat() / resultBounds.height() + assertThat(resultAspectRatio).isWithin(0.1f).of(originalAspectRatio) + + // availableLayoutBounds will be Rect(20, 20, 980, 1980) after subtracting the margins. + // Check if the task is centered within effective layout bounds + val expectedTaskRect = Rect(25, 287, 975, 1713) + assertThat(result) + .isEqualTo(listOf(DesktopTaskBoundsData(taskId = 1, bounds = expectedTaskRect))) + } + + @Test + fun test_multiTasks_formRows() { + val desktopBounds = Rect(0, 0, 1000, 2000) + // Make tasks wide enough so they likely won't all fit in one row + val taskRect = Rect(0, 0, 600, 400) + val taskBounds = + listOf( + DesktopTaskBoundsData(1, taskRect), + DesktopTaskBoundsData(2, taskRect), + DesktopTaskBoundsData(3, taskRect), + ) + + val result = useCase.invoke(desktopBounds, taskBounds, testLayoutConfig) + assertThat(result).hasSize(3) + val bounds1 = result[0].bounds + + // Basic checks: positive dimensions, aspect ratio + result.forEachIndexed { index, data -> + assertThat(data.bounds.width()).isGreaterThan(0) + assertThat(data.bounds.height()).isGreaterThan(0) + val originalAspectRatio = taskRect.width().toFloat() / taskRect.height() + val resultAspectRatio = data.bounds.width().toFloat() / data.bounds.height() + assertThat(resultAspectRatio).isWithin(0.1f).of(originalAspectRatio) + } + + // Expected bounds, based on the current implementation. + // The tasks are expected to be arranged in 3 rows. + val expectedTask1Bounds = Rect(20, 30, 980, 670) + val expectedTask2Bounds = Rect(20, 680, 980, 1320) + val expectedTask3Bounds = Rect(20, 1330, 980, 1970) + val expectedResult = + listOf( + DesktopTaskBoundsData(1, expectedTask1Bounds), + DesktopTaskBoundsData(2, expectedTask2Bounds), + DesktopTaskBoundsData(3, expectedTask3Bounds), + ) + assertThat(result).isEqualTo(expectedResult) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt new file mode 100644 index 0000000000..db78449b2f --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.mapper + +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Surface +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.Flags +import com.android.launcher3.R +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.quickstep.recents.ui.viewmodel.TaskData +import com.android.quickstep.task.apptimer.TaskAppTimerUiState +import com.android.quickstep.task.thumbnail.TaskHeaderUiState +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile +import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import java.time.Duration +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TaskUiStateMapperTest { + + @get:Rule val mSetFlagsRule = SetFlagsRule() + + /** TaskHeaderUiState */ + @Test + fun taskData_isNull_returns_HideHeader() { + val result = + TaskUiStateMapper.toTaskHeaderState( + taskData = null, + hasHeader = false, + clickCloseListener = null, + ) + assertThat(result).isEqualTo(TaskHeaderUiState.HideHeader) + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun explodedFlagDisabled_returnsHideHeader() { + val inputs = + listOf( + TASK_DATA, + TASK_DATA.copy(thumbnailData = null), + TASK_DATA.copy(isLocked = true), + TASK_DATA.copy(title = null), + ) + val closeCallback = View.OnClickListener {} + val expected = TaskHeaderUiState.HideHeader + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskHeaderState( + taskData = taskData, + hasHeader = true, + clickCloseListener = closeCallback, + ) + assertThat(result).isEqualTo(expected) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_hasHeader_and_taskData_returnsShowHeader() { + val inputs = + listOf( + TASK_DATA.copy(isLiveTile = true), + TASK_DATA.copy(isLiveTile = true, thumbnailData = null), + TASK_DATA.copy(isLiveTile = true, isLocked = true), + TASK_DATA.copy(isLiveTile = true, title = null), + ) + val closeCallback = View.OnClickListener {} + val expected = + TaskHeaderUiState.ShowHeader( + header = + TaskHeaderUiState.ThumbnailHeader( + TASK_ICON, + TASK_TITLE_DESCRIPTION, + closeCallback, + ) + ) + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskHeaderState( + taskData = taskData, + hasHeader = true, + clickCloseListener = closeCallback, + ) + assertThat(result).isEqualTo(expected) + } + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_EXPLODED_VIEW) + @Test + fun taskData_hasHeader_emptyTaskData_returns_HideHeader() { + val inputs = + listOf( + TASK_DATA.copy(isLiveTile = true, icon = null), + TASK_DATA.copy(isLiveTile = true, titleDescription = null), + TASK_DATA.copy(isLiveTile = true, icon = null, titleDescription = null), + ) + + inputs.forEach { taskData -> + val result = + TaskUiStateMapper.toTaskHeaderState( + taskData = taskData, + hasHeader = true, + clickCloseListener = {}, + ) + assertThat(result).isEqualTo(TaskHeaderUiState.HideHeader) + } + } + + /** TaskThumbnailUiState */ + @Test + fun taskData_isNull_returns_Uninitialized() { + val result = TaskUiStateMapper.toTaskThumbnailUiState(taskData = null) + assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized) + } + + @Test + fun taskData_isLiveTile_returns_LiveTile() { + val inputs = + listOf( + TASK_DATA.copy(isLiveTile = true), + TASK_DATA.copy(isLiveTile = true, thumbnailData = null), + TASK_DATA.copy(isLiveTile = true, isLocked = true), + ) + inputs.forEach { input -> + val result = TaskUiStateMapper.toTaskThumbnailUiState(taskData = input) + assertThat(result).isEqualTo(LiveTile) + } + } + + @Test + fun taskData_isStaticTile_returns_SnapshotSplash() { + val result = TaskUiStateMapper.toTaskThumbnailUiState(taskData = TASK_DATA) + + val expected = + TaskThumbnailUiState.SnapshotSplash( + snapshot = + Snapshot( + backgroundColor = TASK_BACKGROUND_COLOR, + bitmap = TASK_THUMBNAIL, + thumbnailRotation = Surface.ROTATION_0, + ), + splash = TASK_ICON, + ) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun taskData_thumbnailIsNull_returns_BackgroundOnly() { + val result = + TaskUiStateMapper.toTaskThumbnailUiState( + taskData = TASK_DATA.copy(thumbnailData = null) + ) + + val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR) + assertThat(result).isEqualTo(expected) + } + + @Test + fun taskData_isLocked_returns_BackgroundOnly() { + val result = + TaskUiStateMapper.toTaskThumbnailUiState(taskData = TASK_DATA.copy(isLocked = true)) + + val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR) + assertThat(result).isEqualTo(expected) + } + + @Test + fun toTaskAppTimer_nullTaskData_returnsUninitialized() { + val result = + TaskUiStateMapper.toTaskAppTimerUiState( + canShowAppTimer = true, + stagePosition = STAGE_POSITION_DEFAULT, + taskData = null, + ) + + val expected = TaskAppTimerUiState.Uninitialized + assertThat(result).isEqualTo(expected) + } + + @Test + fun toTaskAppTimer_noTaskData_returnsUninitialized() { + val result = + TaskUiStateMapper.toTaskAppTimerUiState( + canShowAppTimer = true, + stagePosition = STAGE_POSITION_DEFAULT, + taskData = TaskData.NoData(TASK_ID), + ) + + val expected = TaskAppTimerUiState.Uninitialized + assertThat(result).isEqualTo(expected) + } + + @Test + fun toTaskAppTimer_canShowAppTimerFalse_returnsNoTimer() { + val result = + TaskUiStateMapper.toTaskAppTimerUiState( + canShowAppTimer = false, + stagePosition = STAGE_POSITION_DEFAULT, + taskData = TASK_DATA, + ) + + val expected = TaskAppTimerUiState.NoTimer(taskDescription = TASK_TITLE_DESCRIPTION) + assertThat(result).isEqualTo(expected) + } + + @Test + fun toTaskAppTimer_timerNullAndCanShow_returnsNoTimer() { + val result = + TaskUiStateMapper.toTaskAppTimerUiState( + canShowAppTimer = false, + stagePosition = STAGE_POSITION_DEFAULT, + taskData = TASK_DATA.copy(remainingAppTimerDuration = null), + ) + + val expected = TaskAppTimerUiState.NoTimer(taskDescription = TASK_TITLE_DESCRIPTION) + assertThat(result).isEqualTo(expected) + } + + @Test + fun toTaskAppTimer_timerPresentAndCanShow_returnsTimer() { + val result = + TaskUiStateMapper.toTaskAppTimerUiState( + canShowAppTimer = true, + stagePosition = STAGE_POSITION_DEFAULT, + taskData = TASK_DATA.copy(remainingAppTimerDuration = TASK_APP_TIMER_DURATION), + ) + + val expected = + TaskAppTimerUiState.Timer( + timeRemaining = TASK_APP_TIMER_DURATION, + taskDescription = TASK_DATA.titleDescription, + taskPackageName = TASK_DATA.packageName, + accessibilityActionId = R.id.action_digital_wellbeing_top_left, + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun toTaskAppTimer_stagePositionBottomOrRight_returnsTimerWithCorrectActionId() { + val result = + TaskUiStateMapper.toTaskAppTimerUiState( + canShowAppTimer = true, + stagePosition = STAGE_POSITION_BOTTOM_OR_RIGHT, + taskData = TASK_DATA.copy(remainingAppTimerDuration = TASK_APP_TIMER_DURATION), + ) + + val expected = + TaskAppTimerUiState.Timer( + timeRemaining = TASK_APP_TIMER_DURATION, + taskDescription = TASK_DATA.titleDescription, + taskPackageName = TASK_DATA.packageName, + accessibilityActionId = R.id.action_digital_wellbeing_bottom_right, + ) + assertThat(result).isEqualTo(expected) + } + + private companion object { + const val TASK_TITLE_DESCRIPTION = "Title Description 1" + var TASK_ID = 1 + var PACKAGE_NAME = "com.test" + val TASK_ICON = ShapeDrawable() + val TASK_THUMBNAIL = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val TASK_THUMBNAIL_DATA = + ThumbnailData(thumbnail = TASK_THUMBNAIL, rotation = Surface.ROTATION_0) + val TASK_BACKGROUND_COLOR = Color.rgb(1, 2, 3) + val TASK_APP_TIMER_DURATION: Duration = Duration.ofMillis(30) + val STAGE_POSITION_DEFAULT = STAGE_POSITION_TOP_OR_LEFT + val TASK_DATA = + TaskData.Data( + TASK_ID, + packageName = PACKAGE_NAME, + title = "Task 1", + titleDescription = TASK_TITLE_DESCRIPTION, + icon = TASK_ICON, + thumbnailData = TASK_THUMBNAIL_DATA, + backgroundColor = TASK_BACKGROUND_COLOR, + isLocked = false, + isLiveTile = false, + remainingAppTimerDuration = TASK_APP_TIMER_DURATION, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt new file mode 100644 index 0000000000..b499a1d66e --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.recents.ui.viewmodel + +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV +import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS +import com.android.launcher3.util.TestDispatcherProvider +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.quickstep.recents.domain.model.TaskModel +import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase +import com.android.quickstep.recents.domain.usecase.GetTaskUseCase +import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase +import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase +import com.android.quickstep.recents.viewmodel.RecentsViewData +import com.android.quickstep.views.TaskViewType +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import java.time.Duration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class TaskViewModelTest { + private val unconfinedTestDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(unconfinedTestDispatcher) + + private val recentsViewData = RecentsViewData() + private val getTaskUseCase = mock() + private val getThumbnailPositionUseCase = mock() + private val isThumbnailValidUseCase = + spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository())) + private lateinit var sut: TaskViewModel + + @Before + fun setUp() { + sut = createTaskViewModel(TaskViewType.SINGLE) + whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) }) + whenever(getTaskUseCase.invoke(TASK_MODEL_2.id)).thenReturn(flow { emit(TASK_MODEL_2) }) + whenever(getTaskUseCase.invoke(TASK_MODEL_3.id)).thenReturn(flow { emit(TASK_MODEL_3) }) + whenever(getTaskUseCase.invoke(TASK_MODEL_MINIMIZED.id)) + .thenReturn(flow { emit(TASK_MODEL_MINIMIZED) }) + whenever(getTaskUseCase.invoke(INVALID_TASK_ID)).thenReturn(flow { emit(null) }) + recentsViewData.runningTaskIds.value = emptySet() + } + + @Test + fun singleTaskRetrieved_when_validTaskId() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + val expectedResult = + TaskTileUiState( + tasks = listOf(TASK_MODEL_1.toUiState()), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun hasHeader_when_taskViewTypeIsDesktop() = + testScope.runTest { + val expectedResults = + mapOf( + TaskViewType.SINGLE to false, + TaskViewType.GROUPED to false, + TaskViewType.DESKTOP to true, + ) + + expectedResults.forEach { (type, expectedResult) -> + sut = + TaskViewModel( + taskViewType = type, + recentsViewData = recentsViewData, + getTaskUseCase = getTaskUseCase, + getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(), + isThumbnailValidUseCase = isThumbnailValidUseCase, + getThumbnailPositionUseCase = getThumbnailPositionUseCase, + dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher), + ) + sut.bind(TASK_MODEL_1.id) + assertThat(sut.state.first().hasHeader).isEqualTo(expectedResult) + } + } + + @Test + fun multipleTasksRetrieved_when_validTaskIds() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id, INVALID_TASK_ID) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(), + TASK_MODEL_2.toUiState(), + TASK_MODEL_3.toUiState(), + TaskData.NoData(INVALID_TASK_ID), + ), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isLiveTile_when_runningTasksMatchTasks() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = false + recentsViewData.runningTaskIds.value = + setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(isLiveTile = true), + TASK_MODEL_2.toUiState(isLiveTile = true), + TASK_MODEL_3.toUiState(isLiveTile = true), + ), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isMixedLiveTile_when_oneTaskIsMinimized() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = false + recentsViewData.runningTaskIds.value = + setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id, TASK_MODEL_MINIMIZED.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id, TASK_MODEL_MINIMIZED.id) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(isLiveTile = true), + TASK_MODEL_2.toUiState(isLiveTile = true), + TASK_MODEL_3.toUiState(isLiveTile = true), + TASK_MODEL_MINIMIZED.toUiState(isLiveTile = false), + ), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isNotLiveTile_when_runningTaskShowScreenshotIsTrue() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = true + recentsViewData.runningTaskIds.value = + setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(), + TASK_MODEL_2.toUiState(), + TASK_MODEL_3.toUiState(), + ), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isNotLiveTile_when_runningTasksMatchPartialTasks_lessRunningTasks() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = false + recentsViewData.runningTaskIds.value = setOf(TASK_MODEL_1.id, TASK_MODEL_2.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + val expectedResult = + TaskTileUiState( + tasks = + listOf( + TASK_MODEL_1.toUiState(), + TASK_MODEL_2.toUiState(), + TASK_MODEL_3.toUiState(), + ), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun isNotLiveTile_when_runningTasksMatchPartialTasks_moreRunningTasks() = + testScope.runTest { + recentsViewData.runningTaskShowScreenshot.value = false + recentsViewData.runningTaskIds.value = + setOf(TASK_MODEL_1.id, TASK_MODEL_2.id, TASK_MODEL_3.id) + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id) + val expectedResult = + TaskTileUiState( + tasks = listOf(TASK_MODEL_1.toUiState(), TASK_MODEL_2.toUiState()), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_LIGHT_THEME, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun noDataAvailable_when_InvalidTaskId() = + testScope.runTest { + sut.bind(INVALID_TASK_ID) + val expectedResult = + TaskTileUiState( + listOf(TaskData.NoData(INVALID_TASK_ID)), + hasHeader = false, + sysUiStatusNavFlags = FLAGS_APPEARANCE_DEFAULT, + taskOverlayEnabled = false, + isCentralTask = false, + ) + assertThat(sut.state.first()).isEqualTo(expectedResult) + } + + @Test + fun taskOverlayEnabled_when_OverlayIsEnabledForVisibleSingleTask() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(1) + + assertThat(sut.state.first().taskOverlayEnabled).isTrue() + } + + @Test + fun taskOverlayDisabled_when_OverlayIsEnabledForInvisibleTask() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(2) + + assertThat(sut.state.first().taskOverlayEnabled).isFalse() + } + + @Test + fun taskOverlayDisabled_when_OverlayIsDisabledForVisibleTask() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = false + recentsViewData.settledFullyVisibleTaskIds.value = setOf(1) + + assertThat(sut.state.first().taskOverlayEnabled).isFalse() + } + + @Test + fun taskOverlayDisabled_when_OverlayIsEnabledForVisibleDesktopTask() = + testScope.runTest { + sut = createTaskViewModel(TaskViewType.DESKTOP) + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(1) + + assertThat(sut.state.first().taskOverlayEnabled).isFalse() + } + + @Test + fun taskOverlayDisabled_when_OverlayIsEnabledForVisibleGroupedTask() = + testScope.runTest { + sut = createTaskViewModel(TaskViewType.GROUPED) + sut.bind(TASK_MODEL_1.id) + recentsViewData.overlayEnabled.value = true + recentsViewData.settledFullyVisibleTaskIds.value = setOf(1) + + assertThat(sut.state.first().taskOverlayEnabled).isFalse() + } + + @Test + fun isCentralTask_when_CentralTaskIdsMatchTaskIds() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id) + recentsViewData.centralTaskIds.value = setOf(TASK_MODEL_1.id, TASK_MODEL_2.id) + + assertThat(sut.state.first().isCentralTask).isTrue() + } + + @Test + fun isNotCentralTask_when_CentralTaskIdsDoMatchTaskIds() = + testScope.runTest { + sut.bind(TASK_MODEL_1.id, TASK_MODEL_2.id) + recentsViewData.centralTaskIds.value = setOf(TASK_MODEL_3.id) + + assertThat(sut.state.first().isCentralTask).isFalse() + } + + @Test + fun shouldShowSplash_calls_useCase() { + sut.isThumbnailValid(null, 0, 0) + verify(isThumbnailValidUseCase).invoke(anyOrNull(), anyInt(), anyInt()) + } + + private fun TaskModel.toUiState(isLiveTile: Boolean = false) = + TaskData.Data( + taskId = id, + packageName = packageName, + title = title, + titleDescription = titleDescription, + icon = icon!!, + thumbnailData = thumbnail, + backgroundColor = backgroundColor, + isLocked = isLocked, + isLiveTile = isLiveTile, + remainingAppTimerDuration = remainingAppDuration, + ) + + private fun createTaskViewModel(taskViewType: TaskViewType) = + TaskViewModel( + taskViewType = taskViewType, + recentsViewData = recentsViewData, + getTaskUseCase = getTaskUseCase, + getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(), + isThumbnailValidUseCase = isThumbnailValidUseCase, + getThumbnailPositionUseCase = getThumbnailPositionUseCase, + dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher), + ) + + private companion object { + const val PACKAGE_NAME = "com.test" + const val INVALID_TASK_ID = -1 + const val FLAGS_APPEARANCE_LIGHT_THEME = FLAG_LIGHT_STATUS or FLAG_LIGHT_NAV + const val FLAGS_APPEARANCE_DEFAULT = 0 + const val APPEARANCE_LIGHT_THEME = + APPEARANCE_LIGHT_CAPTION_BARS or + APPEARANCE_LIGHT_STATUS_BARS or + APPEARANCE_LIGHT_NAVIGATION_BARS + + val TASK_MODEL_1 = + TaskModel( + 1, + PACKAGE_NAME, + "Title 1", + "Content Description 1", + ShapeDrawable(), + ThumbnailData(appearance = APPEARANCE_LIGHT_THEME), + Color.BLACK, + /* isLocked= */ false, + /* isMinimized= */ false, + /*remainingAppDuration= */ Duration.ofMillis(30), + ) + val TASK_MODEL_2 = + TaskModel( + 2, + PACKAGE_NAME, + "Title 2", + "Content Description 2", + ShapeDrawable(), + ThumbnailData(appearance = APPEARANCE_LIGHT_THEME), + Color.RED, + /* isLocked= */ true, + /* isMinimized= */ false, + /*remainingAppDuration= */ Duration.ofHours(5).plusMinutes(2), + ) + val TASK_MODEL_3 = + TaskModel( + 3, + PACKAGE_NAME, + "Title 3", + "Content Description 3", + ShapeDrawable(), + ThumbnailData(appearance = APPEARANCE_LIGHT_THEME), + Color.BLUE, + /* isLocked= */ false, + /* isMinimized= */ false, + /* remainingAppDuration= */ null, + ) + val TASK_MODEL_MINIMIZED = + TaskModel( + 4, + PACKAGE_NAME, + "Title 4", + "Content Description 4", + ShapeDrawable(), + ThumbnailData(appearance = APPEARANCE_LIGHT_THEME), + Color.BLUE, + /* isLocked= */ false, + /* isMinimized= */ true, + /* remainingAppDuration= */ null, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt new file mode 100644 index 0000000000..c53f20ffdc --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt @@ -0,0 +1,140 @@ +/* + * 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.quickstep.recents.viewmodel + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.view.Display.DEFAULT_DISPLAY +import android.view.Surface +import com.android.quickstep.recents.data.FakeTasksRepository +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class RecentsViewModelTest { + private val tasksRepository = FakeTasksRepository() + private val recentsViewData = RecentsViewData() + private val systemUnderTest = + RecentsViewModel(tasksRepository, recentsViewData, DEFAULT_DISPLAY) + + private val tasks = (0..5).map(::createTaskWithId) + + @Test + fun taskVisibilityControlThumbnailsAvailability() = runTest { + val thumbnailData1 = createThumbnailData() + val thumbnailData2 = createThumbnailData() + tasksRepository.seedTasks(tasks) + tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2)) + + val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1) + val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2) + + systemUnderTest.refreshAllTaskData() + + assertThat(thumbnailDataFlow1.first()).isNull() + assertThat(thumbnailDataFlow2.first()).isNull() + + systemUnderTest.updateVisibleTasks(listOf(1, 2)) + + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2) + + systemUnderTest.updateVisibleTasks(listOf(1)) + + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()).isNull() + + systemUnderTest.onReset() + + assertThat(thumbnailDataFlow1.first()).isNull() + assertThat(thumbnailDataFlow2.first()).isNull() + } + + @Test + fun updatesRunningTaskShowScreenshot() = runTest { + systemUnderTest.setRunningTaskShowScreenshot(true) + systemUnderTest.waitForRunningTaskShowScreenshotToUpdate() + } + + @Test + fun waitForThumbnailsToUpdate() = runTest { + // Given taskRepository with visible 2 tasks containing thumbnailData + val thumbnailData1 = createThumbnailData().apply { snapshotId = 1 } + val thumbnailData2 = createThumbnailData().apply { snapshotId = 2 } + tasksRepository.seedTasks(tasks) + tasksRepository.seedThumbnailData(mapOf(1 to thumbnailData1, 2 to thumbnailData2)) + systemUnderTest.updateVisibleTasks(listOf(1, 2)) + + val thumbnailDataFlow1 = tasksRepository.getThumbnailById(1) + val thumbnailDataFlow2 = tasksRepository.getThumbnailById(2) + + // Then getThumbnailById should initially contains correct thumbnailData + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()).isEqualTo(thumbnailData2) + + // When thumbnailData is updated in taskRepository + tasksRepository.seedThumbnailData( + mapOf(1 to thumbnailData1, 2 to createThumbnailData().apply { snapshotId = 3 }) + ) + // setVisibleTasks forces FakeTasksRepository to update the flows returned by + // getThumbnailById + tasksRepository.setVisibleTasks(DEFAULT_DISPLAY, setOf(1, 2)) + + // Then wait for thumbnailData should complete, and the previous getThumbnailById flow + // should return updated values + systemUnderTest.waitForThumbnailsToUpdate( + mapOf(2 to createThumbnailData().apply { snapshotId = 3 }) + ) + assertThat(thumbnailDataFlow1.first()).isEqualTo(thumbnailData1) + assertThat(thumbnailDataFlow2.first()?.snapshotId).isEqualTo(3) + } + + @Test + fun waitForThumbnailsToUpdate_emptyMap() = runTest { + systemUnderTest.waitForThumbnailsToUpdate(emptyMap()) + } + + @Test + fun waitForThumbnailsToUpdate_null() = runTest { + systemUnderTest.waitForThumbnailsToUpdate(null) + } + + private fun createTaskWithId(taskId: Int) = + Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { + colorBackground = Color.argb(taskId, taskId, taskId, taskId) + } + + private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData { + val bitmap = mock() + whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH) + whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT) + + return ThumbnailData(thumbnail = bitmap, rotation = rotation) + } + + companion object { + const val THUMBNAIL_WIDTH = 100 + const val THUMBNAIL_HEIGHT = 200 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/apptimer/DurationFormatterTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/apptimer/DurationFormatterTest.kt new file mode 100644 index 0000000000..685927fada --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/apptimer/DurationFormatterTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.task.apptimer + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.R +import com.android.launcher3.util.SandboxApplication +import com.google.common.truth.Truth.assertThat +import java.time.Duration +import java.util.Locale +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DurationFormatterTest { + @get:Rule val context = SandboxApplication() + + private var systemLocale: Locale? = null + + @Before + fun setup() { + systemLocale = Locale.getDefault() + val testLocale = Locale("en", "us") + Locale.setDefault(testLocale) + } + + @Test + fun getReadableDuration_hasHoursAndMinutes_returnsNarrowString() { + val result = + DurationFormatter.format( + context, + Duration.ofHours(12).plusMinutes(55), + durationLessThanOneMinuteStringId = R.string.shorter_duration_less_than_one_minute, + ) + + val expected = "12h 55m" + assertThat(result).isEqualTo(expected) + } + + @Test + fun getReadableDuration_hasFullHours_returnsWideString() { + val result = + DurationFormatter.format( + context = context, + duration = Duration.ofHours(12), + durationLessThanOneMinuteStringId = R.string.shorter_duration_less_than_one_minute, + ) + + val expected = "12 hours" + assertThat(result).isEqualTo(expected) + } + + @Test + fun getReadableDuration_hasFullMinutesNoHours_returnsWideString() { + val result = + DurationFormatter.format( + context = context, + duration = Duration.ofMinutes(50), + durationLessThanOneMinuteStringId = R.string.shorter_duration_less_than_one_minute, + ) + + val expected = "50 minutes" + assertThat(result).isEqualTo(expected) + } + + @After + fun tearDown() { + Locale.setDefault(systemLocale) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt deleted file mode 100644 index 3b8754c200..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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.quickstep.task.thumbnail - -import android.content.ComponentName -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.Color -import android.graphics.Rect -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.quickstep.recents.data.FakeTasksRepository -import com.android.quickstep.recents.viewmodel.RecentsViewData -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot -import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized -import com.android.quickstep.task.viewmodel.TaskViewData -import com.android.systemui.shared.recents.model.Task -import com.android.systemui.shared.recents.model.ThumbnailData -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever - -@RunWith(AndroidJUnit4::class) -class TaskThumbnailViewModelTest { - private val recentsViewData = RecentsViewData() - private val taskViewData = TaskViewData() - private val tasksRepository = FakeTasksRepository() - private val systemUnderTest = - TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository) - - private val tasks = (0..5).map(::createTaskWithId) - - @Test - fun initialStateIsUninitialized() = runTest { - assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) - } - - @Test - fun bindRunningTask_thenStateIs_LiveTile() = runTest { - tasksRepository.seedTasks(tasks) - val taskThumbnail = TaskThumbnail(taskId = 1, isRunning = true) - systemUnderTest.bind(taskThumbnail) - - assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile) - } - - @Test - fun setRecentsFullscreenProgress_thenProgressIsPassedThrough() = runTest { - recentsViewData.fullscreenProgress.value = 0.5f - - assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.5f) - - recentsViewData.fullscreenProgress.value = 0.6f - - assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.6f) - } - - @Test - fun setAncestorScales_thenScaleIsCalculated() = runTest { - recentsViewData.scale.value = 0.5f - taskViewData.scale.value = 0.6f - - assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f) - } - - @Test - fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() = - runTest { - tasksRepository.seedTasks(tasks) - val runningTask = TaskThumbnail(taskId = 1, isRunning = true) - val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false) - systemUnderTest.bind(runningTask) - assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile) - - systemUnderTest.bind(stoppedTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - } - - @Test - fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest { - tasksRepository.seedTasks(tasks) - val stoppedTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(stoppedTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - } - - @Test - fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest { - tasksRepository.seedThumbnailData(mapOf(2 to createThumbnailData())) - tasks[2].isLocked = true - tasksRepository.seedTasks(tasks) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(recentTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - } - - @Test - fun bindStoppedTaskWithThumbnail_thenStateIs_Snapshot_withAlphaRemoved() = runTest { - val expectedThumbnailData = createThumbnailData() - tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData)) - tasksRepository.seedTasks(tasks) - tasksRepository.setVisibleTasks(listOf(2)) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(recentTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo( - Snapshot( - backgroundColor = Color.rgb(2, 2, 2), - bitmap = expectedThumbnailData.thumbnail!!, - drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) - ) - ) - } - - @Test - fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshot() = runTest { - val expectedThumbnailData = createThumbnailData() - tasksRepository.seedThumbnailData(mapOf(2 to expectedThumbnailData)) - tasksRepository.seedTasks(tasks) - val recentTask = TaskThumbnail(taskId = 2, isRunning = false) - - systemUnderTest.bind(recentTask) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2))) - tasksRepository.setVisibleTasks(listOf(2)) - assertThat(systemUnderTest.uiState.first()) - .isEqualTo( - Snapshot( - backgroundColor = Color.rgb(2, 2, 2), - bitmap = expectedThumbnailData.thumbnail!!, - drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) - ) - ) - } - - private fun createTaskWithId(taskId: Int) = - Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { - colorBackground = Color.argb(taskId, taskId, taskId, taskId) - } - - private fun createThumbnailData(): ThumbnailData { - val bitmap = mock() - whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH) - whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT) - - return ThumbnailData(thumbnail = bitmap) - } - - companion object { - const val THUMBNAIL_WIDTH = 100 - const val THUMBNAIL_HEIGHT = 200 - } -} diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt similarity index 87% rename from quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt index 4d10f0f51e..f95d2f7d22 100644 --- a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt @@ -55,11 +55,10 @@ class TaskbarPinningControllerTest : TaskbarBaseTestCase() { private val taskbarDragLayer = mock() private val taskbarSharedState = mock() private var isInDesktopMode = false - private val isInDesktopModeProvider = { isInDesktopMode } private val launcherPrefs = - mock { - on { get(TASKBAR_PINNING) } doReturn false - on { get(TASKBAR_PINNING_IN_DESKTOP_MODE) } doReturn false + mock().apply { + doReturn(false).whenever(this).get(TASKBAR_PINNING) + doReturn(false).whenever(this).get(TASKBAR_PINNING_IN_DESKTOP_MODE) } private val statsLogger = mock() private val statsLogManager = mock { on { logger() } doReturn statsLogger } @@ -71,8 +70,26 @@ class TaskbarPinningControllerTest : TaskbarBaseTestCase() { whenever(taskbarActivityContext.launcherPrefs).thenReturn(launcherPrefs) whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer) whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager) - pinningController = - spy(TaskbarPinningController(taskbarActivityContext, isInDesktopModeProvider)) + whenever( + taskbarControllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview( + taskbarActivityContext.displayId + ) + ) + .thenAnswer { _ -> isInDesktopMode } + whenever( + taskbarControllers.taskbarDesktopModeController.isInDesktopMode( + taskbarActivityContext.displayId + ) + ) + .thenAnswer { _ -> isInDesktopMode } + whenever( + taskbarControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar( + taskbarActivityContext.displayId + ) + ) + .thenAnswer { _ -> isInDesktopMode } + + pinningController = spy(TaskbarPinningController(taskbarActivityContext)) pinningController.init(taskbarControllers, taskbarSharedState) } @@ -113,6 +130,14 @@ class TaskbarPinningControllerTest : TaskbarBaseTestCase() { verify(pinningController, times(1)).animateTaskbarPinning(PINNING_PERSISTENT) } + @Test + fun testOnCloseCallback_whenLauncherPreferenceChanged_shouldNotAnimateToTaskbarInDesktopMode() { + isInDesktopMode = true + whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false) + pinningController.onCloseCallback(true) + verify(pinningController, never()).animateTaskbarPinning(any()) + } + @Test fun testOnCloseCallback_whenLauncherPreferenceChanged_shouldAnimateToTransientTaskbar() { whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(true) @@ -207,13 +232,4 @@ class TaskbarPinningControllerTest : TaskbarBaseTestCase() { assertThat(pinningController.isAnimatingTaskbarPinning).isFalse() verify(launcherPrefs, times(1)).put(TASKBAR_PINNING, true) } - - @Test - fun testRecreateTaskbarAndUpdatePinningValue_whenAnimationEnds_shouldUpdateTaskbarPinningDesktopModePref() { - isInDesktopMode = true - pinningController.recreateTaskbarAndUpdatePinningValue() - verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(false) - assertThat(pinningController.isAnimatingTaskbarPinning).isFalse() - verify(launcherPrefs, times(1)).put(TASKBAR_PINNING_IN_DESKTOP_MODE, true) - } } diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt similarity index 83% rename from quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt index b637e7d4cf..d66197a6fb 100644 --- a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt @@ -16,6 +16,7 @@ package com.android.quickstep.taskbar.customization +import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.taskbar.customization.TaskbarFeatureEvaluator import com.android.launcher3.taskbar.customization.TaskbarIconSpecs import com.android.launcher3.taskbar.customization.TaskbarSpecsEvaluator @@ -32,15 +33,26 @@ import org.mockito.kotlin.whenever class TaskbarSpecsEvaluatorTest { private val taskbarFeatureEvaluator = mock() - private val taskbarSpecsEvaluator = spy(TaskbarSpecsEvaluator(taskbarFeatureEvaluator)) + private val taskbarActivityContext = mock() + private var taskbarSpecsEvaluator = + spy(TaskbarSpecsEvaluator(taskbarActivityContext, taskbarFeatureEvaluator, 0, 0)) @Test - fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumn() { + fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumnInLandscape() { doReturn(true).whenever(taskbarFeatureEvaluator).isTransient - assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(6, 5)) + doReturn(true).whenever(taskbarFeatureEvaluator).isLandscape + assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(4, 4)) .isEqualTo(TaskbarIconSpecs.iconSize52dp) } + @Test + fun testGetIconSizeByGrid_whenTaskbarIsTransient_withValidRowAndColumnInPortrait() { + doReturn(true).whenever(taskbarFeatureEvaluator).isTransient + doReturn(false).whenever(taskbarFeatureEvaluator).isLandscape + assertThat(taskbarSpecsEvaluator.getIconSizeByGrid(4, 4)) + .isEqualTo(TaskbarIconSpecs.iconSize48dp) + } + @Test fun testGetIconSizeByGrid_whenTaskbarIsTransient_withInvalidRowAndColumn() { doReturn(true).whenever(taskbarFeatureEvaluator).isTransient diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt new file mode 100644 index 0000000000..b4c236e996 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.hardware.input.InputManager +import android.view.InputDevice +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHPAD +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.launcher3.util.IntArray +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.TestUtil +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +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.ArgumentMatchers.eq +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class ActiveTrackpadListTest { + + @get:Rule val context = SandboxApplication() + + private val inputDeviceIds = IntArray() + private lateinit var inputManager: InputManager + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + inputManager = context.spyService(InputManager::class.java) + doAnswer { inputDeviceIds.toArray() }.whenever(inputManager).inputDeviceIds + + doReturn(null).whenever(inputManager).getInputDevice(eq(1)) + doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD)) + .whenever(inputManager) + .getInputDevice(eq(2)) + doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD)) + .whenever(inputManager) + .getInputDevice(eq(3)) + doReturn(mockDevice(SOURCE_MOUSE)).whenever(inputManager).getInputDevice(eq(4)) + } + + @Test + fun `initialize correct devices`() { + inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4)) + + val list = ActiveTrackpadList(context) {} + assertEquals(2, list.size()) + assertTrue(list.contains(2)) + assertTrue(list.contains(3)) + } + + @Test + fun `update callback not called in constructor`() { + inputDeviceIds.addAll(IntArray.wrap(2, 3)) + + var updateCalled = false + val list = ActiveTrackpadList(context) { updateCalled = true } + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + + assertEquals(2, list.size()) + assertFalse(updateCalled) + } + + @Test + fun `update called on add only once`() { + var updateCalled = false + val list = ActiveTrackpadList(context) { updateCalled = true } + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + + assertFalse(updateCalled) + assertEquals(0, list.size()) + + list.onInputDeviceAdded(1) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertFalse(updateCalled) + assertEquals(0, list.size()) + + list.onInputDeviceAdded(2) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertTrue(updateCalled) + assertEquals(1, list.size()) + + updateCalled = false + list.onInputDeviceAdded(3) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertFalse(updateCalled) + assertEquals(2, list.size()) + } + + @Test + fun `update called on remove only once`() { + var updateCalled = false + inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4)) + val list = ActiveTrackpadList(context) { updateCalled = true } + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertEquals(2, list.size()) + + list.onInputDeviceRemoved(2) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertEquals(1, list.size()) + assertFalse(updateCalled) + + list.onInputDeviceRemoved(3) + TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {} + assertEquals(0, list.size()) + assertTrue(updateCalled) + } + + private fun mockDevice(sources: Int) = + mock(InputDevice::class.java).apply { doReturn(sources).whenever(this).sources } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt index ece67aff62..c325af4d3e 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt @@ -16,7 +16,10 @@ package com.android.quickstep.util -import android.content.Context +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.res.Resources +import android.view.Display +import android.view.Display.DEFAULT_DISPLAY import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.logging.StatsLogManager @@ -24,13 +27,15 @@ import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.launcher3.views.ActivityContext import com.android.quickstep.TopTaskTracker import com.android.quickstep.TopTaskTracker.CachedTaskInfo import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.Task.TaskKey -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_30_70 -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50 -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_70_30 +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33 import java.util.function.Consumer import org.junit.Assert.assertEquals import org.junit.Before @@ -53,32 +58,34 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class AppPairsControllerTest { - @Mock lateinit var context: Context + @Mock lateinit var context: ActivityContext + @Mock lateinit var resources: Resources @Mock lateinit var splitSelectStateController: SplitSelectStateController @Mock lateinit var statsLogManager: StatsLogManager private lateinit var appPairsController: AppPairsController - private val left30: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_30_70) + private val left33: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_33_66) } private val left50: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_50_50) + appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_50_50) } - private val left70: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_70_30) + private val left66: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_66_33) } - private val right30: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_30_70) + private val right33: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_33_66) } private val right50: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_50_50) + appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_50_50) } - private val right70: Int by lazy { - appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_70_30) + private val right66: Int by lazy { + appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_66_33) } @Mock lateinit var mockAppPairIcon: AppPairIcon + @Mock lateinit var mockDisplay: Display @Mock lateinit var mockTaskbarActivityContext: TaskbarActivityContext @Mock lateinit var mockTopTaskTracker: TopTaskTracker @Mock lateinit var mockCachedTaskInfo: CachedTaskInfo @@ -101,38 +108,42 @@ class AppPairsControllerTest { // Stub methods on appPairsController so that they return mocks spyAppPairsController = spy(appPairsController) whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) + whenever(mockAppPairIcon.display).thenReturn(mockDisplay) + whenever(mockDisplay.displayId).thenReturn(DEFAULT_DISPLAY) doReturn(mockTopTaskTracker).whenever(spyAppPairsController).topTaskTracker - whenever(mockTopTaskTracker.getCachedTopTask(any())).thenReturn(mockCachedTaskInfo) + whenever(mockTopTaskTracker.getCachedTopTask(any(), any())).thenReturn(mockCachedTaskInfo) whenever(mockTask1.getKey()).thenReturn(mockTaskKey1) whenever(mockTask2.getKey()).thenReturn(mockTaskKey2) doNothing().whenever(spyAppPairsController).launchAppPair(any(), any()) doNothing() .whenever(spyAppPairsController) .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + whenever(mockAppPairIcon.context.resources).thenReturn(resources) + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(false) } @Test fun shouldEncodeRankCorrectly() { - assertEquals("left + 30-70 should encode as 0 (0b0)", 0, left30) + assertEquals("left + 33-66 should encode as 0 (0b0)", 0, left33) assertEquals("left + 50-50 should encode as 1 (0b1)", 1, left50) - assertEquals("left + 70-30 should encode as 2 (0b10)", 2, left70) + assertEquals("left + 66-33 should encode as 2 (0b10)", 2, left66) // See AppPairsController#BITMASK_SIZE and BITMASK_FOR_SNAP_POSITION for context - assertEquals("right + 30-70 should encode as 1 followed by 16 0s", 1 shl 16, right30) + assertEquals("right + 33-66 should encode as 1 followed by 16 0s", 1 shl 16, right33) assertEquals("right + 50-50 should encode as the above value + 1", (1 shl 16) + 1, right50) - assertEquals("right + 70-30 should encode as the above value + 2", (1 shl 16) + 2, right70) + assertEquals("right + 66-33 should encode as the above value + 2", (1 shl 16) + 2, right66) } @Test fun shouldDecodeRankCorrectly() { assertEquals( - "left + 30-70 should decode to left", + "left + 33-66 should decode to left", STAGE_POSITION_TOP_OR_LEFT, - AppPairsController.convertRankToStagePosition(left30), + AppPairsController.convertRankToStagePosition(left33), ) assertEquals( - "left + 30-70 should decode to 30-70", - SNAP_TO_30_70, - AppPairsController.convertRankToSnapPosition(left30), + "left + 33-66 should decode to 33-66", + SNAP_TO_2_33_66, + AppPairsController.convertRankToSnapPosition(left33), ) assertEquals( @@ -142,30 +153,30 @@ class AppPairsControllerTest { ) assertEquals( "left + 50-50 should decode to 50-50", - SNAP_TO_50_50, + SNAP_TO_2_50_50, AppPairsController.convertRankToSnapPosition(left50), ) assertEquals( - "left + 70-30 should decode to left", + "left + 66-33 should decode to left", STAGE_POSITION_TOP_OR_LEFT, - AppPairsController.convertRankToStagePosition(left70), + AppPairsController.convertRankToStagePosition(left66), ) assertEquals( - "left + 70-30 should decode to 70-30", - SNAP_TO_70_30, - AppPairsController.convertRankToSnapPosition(left70), + "left + 66-33 should decode to 66-33", + SNAP_TO_2_66_33, + AppPairsController.convertRankToSnapPosition(left66), ) assertEquals( - "right + 30-70 should decode to right", + "right + 33-66 should decode to right", STAGE_POSITION_BOTTOM_OR_RIGHT, - AppPairsController.convertRankToStagePosition(right30), + AppPairsController.convertRankToStagePosition(right33), ) assertEquals( - "right + 30-70 should decode to 30-70", - SNAP_TO_30_70, - AppPairsController.convertRankToSnapPosition(right30), + "right + 33-66 should decode to 33-66", + SNAP_TO_2_33_66, + AppPairsController.convertRankToSnapPosition(right33), ) assertEquals( @@ -175,19 +186,19 @@ class AppPairsControllerTest { ) assertEquals( "right + 50-50 should decode to 50-50", - SNAP_TO_50_50, + SNAP_TO_2_50_50, AppPairsController.convertRankToSnapPosition(right50), ) assertEquals( - "right + 70-30 should decode to right", + "right + 66-33 should decode to right", STAGE_POSITION_BOTTOM_OR_RIGHT, - AppPairsController.convertRankToStagePosition(right70), + AppPairsController.convertRankToStagePosition(right66), ) assertEquals( - "right + 70-30 should decode to 70-30", - SNAP_TO_70_30, - AppPairsController.convertRankToSnapPosition(right70), + "right + 66-33 should decode to 66-33", + SNAP_TO_2_66_33, + AppPairsController.convertRankToSnapPosition(right66), ) } @@ -202,7 +213,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -226,7 +237,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -250,7 +261,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -274,7 +285,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -298,7 +309,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -322,7 +333,7 @@ class AppPairsControllerTest { // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -341,12 +352,16 @@ class AppPairsControllerTest { whenever(mockTaskKey1.getId()).thenReturn(1) whenever(mockTaskKey2.getId()).thenReturn(2) // ... with app 1 already on screen - whenever(mockCachedTaskInfo.taskId).thenReturn(1) + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(1) + } // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -365,12 +380,16 @@ class AppPairsControllerTest { whenever(mockTaskKey1.getId()).thenReturn(1) whenever(mockTaskKey2.getId()).thenReturn(2) // ... with app 2 already on screen - whenever(mockCachedTaskInfo.taskId).thenReturn(2) + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(2) + } // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) @@ -383,18 +402,84 @@ class AppPairsControllerTest { .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT)) } + @Test + fun handleAppPairLaunchInApp_freeformTask1IsOnScreen_shouldLaunchAppPair() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true) + /// Test launching apps 1 and 2 from app pair + whenever(mockTaskKey1.getId()).thenReturn(1) + whenever(mockTaskKey2.getId()).thenReturn(2) + // Task 1 is in freeform windowing mode + mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM + // ... and app 1 is already on screen + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(1) + } + + // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback + spyAppPairsController.handleAppPairLaunchInApp( + mockAppPairIcon, + listOf(mockItemInfo1, mockItemInfo2), + ) + verify(splitSelectStateController) + .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) + val callback: Consumer> = callbackCaptor.value + callback.accept(arrayOf(mockTask1, mockTask2)) + + // Verify that launchAppPair was called + verify(spyAppPairsController, times(1)).launchAppPair(any(), any()) + verify(spyAppPairsController, never()) + .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + + @Test + fun handleAppPairLaunchInApp_freeformTask2IsOnScreen_shouldLaunchAppPair() { + whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true) + /// Test launching apps 1 and 2 from app pair + whenever(mockTaskKey1.getId()).thenReturn(1) + whenever(mockTaskKey2.getId()).thenReturn(2) + // Task 2 is in freeform windowing mode + mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM + // ... and app 2 is already on screen + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(2) + } + + // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback + spyAppPairsController.handleAppPairLaunchInApp( + mockAppPairIcon, + listOf(mockItemInfo1, mockItemInfo2), + ) + verify(splitSelectStateController) + .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) + val callback: Consumer> = callbackCaptor.value + callback.accept(arrayOf(mockTask1, mockTask2)) + + // Verify that launchAppPair was called + verify(spyAppPairsController, times(1)).launchAppPair(any(), any()) + verify(spyAppPairsController, never()) + .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } + @Test fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() { // Test launching apps 1 and 2 from app pair whenever(mockTaskKey1.getId()).thenReturn(1) whenever(mockTaskKey2.getId()).thenReturn(2) // ... with app 3 already on screen - whenever(mockCachedTaskInfo.taskId).thenReturn(3) + if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) { + whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(3))).thenReturn(true) + } else { + whenever(mockCachedTaskInfo.taskId).thenReturn(3) + } // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback spyAppPairsController.handleAppPairLaunchInApp( mockAppPairIcon, - listOf(mockItemInfo1, mockItemInfo2) + listOf(mockItemInfo1, mockItemInfo2), ) verify(splitSelectStateController) .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture()) diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/CachedEventDispatcherTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/CachedEventDispatcherTest.kt new file mode 100644 index 0000000000..296171b866 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/CachedEventDispatcherTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.os.SystemClock +import android.view.MotionEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import java.util.function.Consumer +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +class CachedEventDispatcherTest { + + private lateinit var underTest: CachedEventDispatcher + + @Mock private lateinit var consumer: Consumer + @Captor private lateinit var motionEventCaptor: ArgumentCaptor + + private lateinit var motionEvent: MotionEvent + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + motionEvent = + MotionEvent.obtain( + 0L, + SystemClock.elapsedRealtime(), + MotionEvent.ACTION_DOWN, + 0f, + 0f, + 0, + ) + underTest = CachedEventDispatcher() + } + + @Test + fun testInitialState() { + assertFalse(underTest.hasConsumer()) + } + + @Test + fun dispatchMotionEvents() { + underTest.setConsumer(consumer) + + underTest.dispatchEvent(motionEvent) + + assertTrue(underTest.hasConsumer()) + verify(consumer).accept(motionEventCaptor.capture()) + assertTrue(isMotionEventSame(motionEventCaptor.value, motionEvent)) + } + + @Test + fun dispatchMotionEvents_after_settingConsumer() { + underTest.dispatchEvent(motionEvent) + + underTest.setConsumer(consumer) + + verify(consumer).accept(motionEventCaptor.capture()) + assertTrue(isMotionEventSame(motionEventCaptor.value, motionEvent)) + } + + @Test + fun clearConsumer_notDispatchToConsumer() { + underTest.setConsumer(consumer) + underTest.dispatchEvent(motionEvent) + reset(consumer) + + underTest.clearConsumer() + + assertFalse(underTest.hasConsumer()) + underTest.dispatchEvent(motionEvent) + verifyNoMoreInteractions(consumer) + } + + private fun isMotionEventSame(e1: MotionEvent, e2: MotionEvent): Boolean { + return e1.action == e2.action && + e1.eventTime == e2.eventTime && + e1.x == e2.x && + e1.y == e2.y + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java new file mode 100644 index 0000000000..88774be33c --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java @@ -0,0 +1,325 @@ +/* + * 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.quickstep.util; + +import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED; +import static com.android.quickstep.util.ContextualSearchInvoker.KEYGUARD_SHOWING_SYSUI_FLAGS; +import static com.android.quickstep.util.ContextualSearchInvoker.SHADE_EXPANDED_SYSUI_FLAGS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.contextualsearch.ContextualSearchManager; +import android.content.Context; +import android.content.pm.PackageManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.launcher3.logging.StatsLogManager; +import com.android.quickstep.BaseContainerInterface; +import com.android.quickstep.DeviceConfigWrapper; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Robolectric unit tests for {@link ContextualSearchInvoker} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContextualSearchInvokerTest { + + private static final int CONTEXTUAL_SEARCH_ENTRY_POINT = 123; + + private @Mock PackageManager mMockPackageManager; + private @Mock ContextualSearchStateManager mMockStateManager; + private @Mock TopTaskTracker mMockTopTaskTracker; + private @Mock SystemUiProxy mMockSystemUiProxy; + private @Mock StatsLogManager mMockStatsLogManager; + private @Mock StatsLogManager.StatsLogger mMockStatsLogger; + private @Mock ContextualSearchHapticManager mMockContextualSearchHapticManager; + private @Mock ContextualSearchManager mMockContextualSearchManager; + private @Mock BaseContainerInterface mMockContainerInterface; + private @Mock RecentsViewContainer mMockRecentsViewContainer; + private @Mock RecentsView mMockRecentsView; + private ContextualSearchInvoker mContextualSearchInvoker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(true); + Context context = spy(getApplicationContext()); + doReturn(mMockPackageManager).when(context).getPackageManager(); + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(0L); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{}); + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(true); + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(true); + when(mMockStatsLogManager.logger()).thenReturn(mMockStatsLogger); + when(mMockContainerInterface.getCreatedContainer()).thenReturn(mMockRecentsViewContainer); + when(mMockRecentsViewContainer.getOverviewPanel()).thenReturn(mMockRecentsView); + + mContextualSearchInvoker = spy(new ContextualSearchInvoker(context, mMockStateManager, + mMockTopTaskTracker, mMockSystemUiProxy, mMockStatsLogManager, + mMockContextualSearchHapticManager, mMockContextualSearchManager + )); + doReturn(mMockContainerInterface).when(mContextualSearchInvoker) + .getRecentsContainerInterface(); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchFeatureIsNotAvailable() { + when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsAvailable() { + assertTrue("Expected invocation checks to succeed", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsNotAvailable() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + assertFalse("Expected invocation to fail when feature is unavailable", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_settingDisabled() { + when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(false); + + assertFalse("Expected invocation checks to fail when setting is disabled", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_notificationShadeIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(SHADE_EXPANDED_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when notification shade is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_keyguardIsShowing() { + when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn( + KEYGUARD_SHOWING_SYSUI_FLAGS); + + assertFalse("Expected invocation checks to fail when keyguard is showing", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_disallowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(false); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertFalse("Expected invocation checks to fail over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_allowed() { + when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(true); + when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3}); + + assertTrue("Expected invocation checks to succeed over split screen", + mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()); + + // Attempt is logged regardless. + verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticEnabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticDisabled() { + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticEnabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still vibrate based on the flag. + verify(mMockContextualSearchHapticManager).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + @Test + public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticDisabled() { + when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false); + + try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) { + // Still expect true since this method doesn't run the checks. + assertTrue("Expected ContextualSearch invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + // Still don't vibrate based on the flag. + verify(mMockContextualSearchHapticManager, never()).vibrateForSearch(); + verify(mMockContextualSearchManager).startContextualSearch( + CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_liveTile() { + when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true); + ArgumentCaptor switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor finishRecentsAnimationCaptor = + ArgumentCaptor.forClass(Runnable.class); + + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture()); + switchToScreenshotCaptor.getValue().run(); + verify(mMockRecentsView).finishRecentsAnimation(anyBoolean(), anyBoolean(), + finishRecentsAnimationCaptor.capture()); + finishRecentsAnimationCaptor.getValue().run(); + verify(mMockContextualSearchManager).startContextualSearch(CONTEXTUAL_SEARCH_ENTRY_POINT); + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_liveTile_failsToSwitchToScreenshot() { + when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true); + ArgumentCaptor switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor finishRecentsAnimationCaptor = + ArgumentCaptor.forClass(Runnable.class); + + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture()); + + // Don't run switchToScreenshot's callback. Therefore, recents animation should not finish. + verify(mMockRecentsView, never()).finishRecentsAnimation(anyBoolean(), anyBoolean(), + finishRecentsAnimationCaptor.capture()); + // And ContextualSearch should not start. + verify(mMockContextualSearchManager, never()).startContextualSearch(anyInt()); + verifyNoMoreInteractions(mMockStatsLogManager); + } + + @Test + public void invokeContextualSearchUncheckedWithHaptic_liveTile_failsToFinishRecentsAnimation() { + when(mMockContainerInterface.isInLiveTileMode()).thenReturn(true); + ArgumentCaptor switchToScreenshotCaptor = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor finishRecentsAnimationCaptor = + ArgumentCaptor.forClass(Runnable.class); + + assertTrue("Expected invocation unchecked to succeed", + mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic( + CONTEXTUAL_SEARCH_ENTRY_POINT)); + verify(mMockRecentsView).switchToScreenshot(switchToScreenshotCaptor.capture()); + switchToScreenshotCaptor.getValue().run(); + verify(mMockRecentsView).finishRecentsAnimation(anyBoolean(), anyBoolean(), + finishRecentsAnimationCaptor.capture()); + // Don't run finishRecentsAnimation's callback. Therefore ContextualSearch should not start. + verify(mMockContextualSearchManager, never()).startContextualSearch(anyInt()); + verifyNoMoreInteractions(mMockStatsLogManager); + } + + private AutoCloseable overrideSearchHapticCommitFlag(boolean value) { + return TestExtensions.overrideNavConfigFlag( + "ENABLE_SEARCH_HAPTIC_COMMIT", + value, + () -> DeviceConfigWrapper.get().getEnableSearchHapticCommit()); + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt new file mode 100644 index 0000000000..15da4d4def --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt @@ -0,0 +1,75 @@ +/* + * 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.quickstep.util + +import android.content.ComponentName +import android.content.Intent +import android.view.Display.DEFAULT_DISPLAY +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.systemui.shared.recents.model.Task +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +class DesktopTaskTest { + + @Test + fun testDesktopTask_sameInstance_isEqual() { + val task = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + assertThat(task).isEqualTo(task) + } + + @Test + fun testDesktopTask_identicalConstructor_isEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testDesktopTask_copy_isEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = task1.copy() + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testDesktopTask_differentDeskIds_isNotEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 1, DEFAULT_DISPLAY, createTasks(1)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testDesktopTask_differentTaskIds_isNotEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(2)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testDesktopTask_differentLength_isNotEqual() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, createTasks(1, 2)) + assertThat(task1).isNotEqualTo(task2) + } + + private fun createTasks(vararg ids: Int): List { + return ids.map { Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 0)) } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt similarity index 87% rename from quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt rename to quickstep/tests/multivalentTests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt index c190cfebab..cbe397b4a1 100644 --- a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt @@ -18,11 +18,12 @@ package com.android.quickstep.util import android.graphics.Rect import android.graphics.Region -import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager +import androidx.test.annotation.UiThreadTest import androidx.test.filters.SmallTest import com.android.launcher3.util.Executors +import com.android.launcher3.util.LauncherMultivalentJUnit import com.android.quickstep.util.GestureExclusionManager.ExclusionListener import org.junit.Before import org.junit.Test @@ -31,11 +32,12 @@ import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.reset import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.verifyNoMoreInteractions /** Unit test for [GestureExclusionManager]. */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@UiThreadTest +@RunWith(LauncherMultivalentJUnit::class) class GestureExclusionManagerTest { @Mock private lateinit var windowManager: IWindowManager @@ -72,7 +74,7 @@ class GestureExclusionManagerTest { underTest.addListener(listener2) awaitTasksCompleted() - verifyZeroInteractions(windowManager) + verifyNoMoreInteractions(windowManager) } @Test @@ -98,7 +100,7 @@ class GestureExclusionManagerTest { underTest.removeListener(listener1) awaitTasksCompleted() - verifyZeroInteractions(windowManager) + verifyNoMoreInteractions(windowManager) } @Test @@ -119,17 +121,17 @@ class GestureExclusionManagerTest { awaitTasksCompleted() underTest.addListener(listener1) awaitTasksCompleted() - verifyZeroInteractions(listener1) + verifyNoMoreInteractions(listener1) underTest.addListener(listener2) awaitTasksCompleted() - verifyZeroInteractions(listener1) + verifyNoMoreInteractions(listener1) verify(listener2).onGestureExclusionChanged(r1, r2) } private fun awaitTasksCompleted() { - Executors.UI_HELPER_EXECUTOR.submit { null }.get() - Executors.MAIN_EXECUTOR.submit { null }.get() + Executors.UI_HELPER_EXECUTOR.submit { null }.get() + Executors.MAIN_EXECUTOR.submit { null }.get() } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt new file mode 100644 index 0000000000..f745134109 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt @@ -0,0 +1,190 @@ +/* + * 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.quickstep.util + +import android.content.ComponentName +import android.content.Intent +import android.graphics.Rect +import android.view.Display.DEFAULT_DISPLAY +import android.view.Display.INVALID_DISPLAY +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.systemui.shared.recents.model.Task +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +class GroupTaskTest { + + @Test + fun testGroupTask_sameInstance_isEqual() { + val task = SingleTask(createTask(1)) + assertThat(task).isEqualTo(task) + } + + @Test + fun testGroupTask_identicalConstructor_isEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = SingleTask(createTask(1)) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_copy_isEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = task1.copy() + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_differentId_isNotEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = SingleTask(createTask(2)) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testGroupTask_equalSplitTasks_isEqual() { + val splitBounds = + SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_2_50_50, + ) + val task1 = SplitTask(createTask(1), createTask(2), splitBounds) + val task2 = SplitTask(createTask(1), createTask(2), splitBounds) + assertThat(task1).isEqualTo(task2) + } + + @Test + fun testGroupTask_differentSplitTasks_isNotEqual() { + val splitBounds1 = + SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_2_50_50, + ) + val splitBounds2 = + SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_2_33_66, + ) + val task1 = SplitTask(createTask(1), createTask(2), splitBounds1) + val task2 = SplitTask(createTask(1), createTask(2), splitBounds2) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testGroupTask_differentType_isNotEqual() { + val task1 = SingleTask(createTask(1)) + val task2 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, listOf(createTask(1))) + assertThat(task1).isNotEqualTo(task2) + } + + @Test + fun testDesktopTask_matchesDisplayId() { + val task1 = DesktopTask(deskId = 0, DEFAULT_DISPLAY, listOf(createTask(1, INVALID_DISPLAY))) + assertThat(task1.matchesDisplayId(DEFAULT_DISPLAY)).isTrue() + assertThat(task1.matchesDisplayId(DISPLAY_2)).isFalse() + val task2 = DesktopTask(deskId = 0, DISPLAY_2, listOf(createTask(1, DISPLAY_2))) + assertThat(task2.matchesDisplayId(DEFAULT_DISPLAY)).isFalse() + assertThat(task2.matchesDisplayId(DISPLAY_2)).isTrue() + val task3 = DesktopTask(deskId = 0, DISPLAY_2, listOf(createTask(1, INVALID_DISPLAY))) + assertThat(task3.matchesDisplayId(DEFAULT_DISPLAY)).isFalse() + assertThat(task3.matchesDisplayId(DISPLAY_2)).isTrue() + } + + @Test + fun testSingleTask_matchesDisplayId() { + val task1 = SingleTask(createTask(1, INVALID_DISPLAY)) + assertThat(task1.matchesDisplayId(DEFAULT_DISPLAY)).isTrue() + assertThat(task1.matchesDisplayId(DISPLAY_2)).isFalse() + val task2 = SingleTask(createTask(1, DISPLAY_2)) + assertThat(task2.matchesDisplayId(DEFAULT_DISPLAY)).isFalse() + assertThat(task2.matchesDisplayId(DISPLAY_2)).isTrue() + val task3 = SingleTask(createTask(1, DEFAULT_DISPLAY)) + assertThat(task3.matchesDisplayId(DEFAULT_DISPLAY)).isTrue() + assertThat(task3.matchesDisplayId(DISPLAY_2)).isFalse() + } + + @Test + fun testSplitTask_matchesDisplayId() { + val splitBounds = + SplitBounds( + Rect(), + Rect(), + 1, + 2, + SplitScreenConstants.SNAP_TO_2_50_50, + ) + val task1 = + SplitTask(createTask(1, INVALID_DISPLAY), createTask(2, INVALID_DISPLAY), splitBounds) + assertThat(task1.matchesDisplayId(DEFAULT_DISPLAY)).isTrue() + assertThat(task1.matchesDisplayId(DISPLAY_2)).isFalse() + val task2 = SplitTask(createTask(1, INVALID_DISPLAY), createTask(2, DISPLAY_2), splitBounds) + assertThat(task2.matchesDisplayId(DEFAULT_DISPLAY)).isTrue() + assertThat(task2.matchesDisplayId(DISPLAY_2)).isFalse() + val task3 = SplitTask(createTask(1, DISPLAY_2), createTask(2, INVALID_DISPLAY), splitBounds) + assertThat(task3.matchesDisplayId(DEFAULT_DISPLAY)).isFalse() + assertThat(task3.matchesDisplayId(DISPLAY_2)).isTrue() + val task4 = + SplitTask(createTask(1, DEFAULT_DISPLAY), createTask(2, DEFAULT_DISPLAY), splitBounds) + assertThat(task4.matchesDisplayId(DEFAULT_DISPLAY)).isTrue() + assertThat(task4.matchesDisplayId(DISPLAY_2)).isFalse() + val task5 = SplitTask(createTask(1, DEFAULT_DISPLAY), createTask(2, DISPLAY_2), splitBounds) + assertThat(task5.matchesDisplayId(DEFAULT_DISPLAY)).isTrue() + assertThat(task5.matchesDisplayId(DISPLAY_2)).isFalse() + val task6 = SplitTask(createTask(1, DISPLAY_2), createTask(2, DEFAULT_DISPLAY), splitBounds) + assertThat(task6.matchesDisplayId(DEFAULT_DISPLAY)).isFalse() + assertThat(task6.matchesDisplayId(DISPLAY_2)).isTrue() + } + + private fun createTask(id: Int, displayId: Int = INVALID_DISPLAY, pkg: String? = null): Task { + val intent = Intent() + pkg.let { intent.setPackage(it) } + return Task( + Task.TaskKey( + id, + 0, + intent, + ComponentName(pkg ?: "", ""), + 0, + 0, + displayId, + null, + 0, + false, + false, + ) + ) + } + + companion object { + const val DISPLAY_2 = 2 + const val PACKAGE = "com.android.launcher3" + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateTest.kt new file mode 100644 index 0000000000..137603f79b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentOrientedStateTest.kt @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.view.Surface +import android.view.Surface.ROTATION_0 +import android.view.Surface.ROTATION_180 +import android.view.Surface.ROTATION_90 +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.quickstep.FallbackActivityInterface +import com.android.quickstep.orientation.RecentsPagedOrientationHandler +import com.android.quickstep.orientation.RecentsPagedOrientationHandler.Companion.PORTRAIT +import com.google.common.truth.Truth.assertWithMessage +import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +data class HideActionsTestCase( + val recentsRotation: Int, + val touchRotation: Int, + val isRotationAllowed: Boolean, + val isFixedLandscape: Boolean, +) + +data class RotationHandlerTestCase( + val displayRotation: Int, + val touchRotation: Int, + val isRotationAllowed: Boolean, + val isFixedLandscape: Boolean, +) { + override fun toString(): String { + return "TestCase(displayRotation=${Surface.rotationToString(displayRotation)}, " + + "touchRotation=${Surface.rotationToString(touchRotation)}, " + + "isRotationAllowed=$isRotationAllowed, " + + "isFixedLandscape=$isFixedLandscape)" + } +} + +/** + * Test all possible inputs to RecentsOrientedState.updateHandler. It tests all possible + * combinations of rotations and relevant methods (two methods that return boolean values) but it + * only provides the expected result when the final rotation is different from ROTATION_0 for + * simplicity. So any case not shown in resultMap you can assume results in ROTATION_0. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class RecentOrientedStateTest { + + companion object { + const val INVALID_ROTATION = -1 + } + + private fun createRecentOrientedState() = + spy( + RecentsOrientedState( + ApplicationProvider.getApplicationContext(), + FallbackActivityInterface.INSTANCE, + ) {} + ) + + private fun rotationHandlerTest( + testCase: RotationHandlerTestCase, + expectedHandler: RecentsPagedOrientationHandler, + ) { + val recentOrientedState = createRecentOrientedState() + whenever(recentOrientedState.isRecentsActivityRotationAllowed) + .thenReturn(testCase.isRotationAllowed) + whenever(recentOrientedState.isLauncherFixedLandscape).thenReturn(testCase.isFixedLandscape) + + recentOrientedState.update(testCase.displayRotation, testCase.touchRotation) + val rotation = recentOrientedState.orientationHandler.rotation + assertWithMessage("$testCase to ${Surface.rotationToString(rotation)},") + .that(rotation) + .isEqualTo(expectedHandler.rotation) + } + + private fun shouldHideActionButtonTest( + testCase: HideActionsTestCase, + hideActionButtonsExpected: Boolean, + ) { + val recentOrientedState = createRecentOrientedState() + whenever(recentOrientedState.recentsActivityRotation).thenReturn(testCase.recentsRotation) + whenever(recentOrientedState.touchRotation).thenReturn(testCase.touchRotation) + whenever(recentOrientedState.isRecentsActivityRotationAllowed) + .thenReturn(testCase.isRotationAllowed) + whenever(recentOrientedState.isLauncherFixedLandscape).thenReturn(testCase.isFixedLandscape) + val res = recentOrientedState.shouldHideActionButtons() + assertWithMessage( + "Test case $testCase generated $res but should be $hideActionButtonsExpected" + ) + .that(res) + .isEqualTo(hideActionButtonsExpected) + } + + @Test + fun `stateId changes with flags`() { + val recentOrientedState1 = createRecentOrientedState() + val recentOrientedState2 = createRecentOrientedState() + assertEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + + recentOrientedState1.setGestureActive(true) + recentOrientedState2.setGestureActive(false) + assertNotEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + + recentOrientedState2.setGestureActive(true) + assertEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + } + + @Test + fun `stateId changes with recents rotation`() { + val recentOrientedState1 = createRecentOrientedState() + val recentOrientedState2 = createRecentOrientedState() + recentOrientedState1.setRecentsRotation(ROTATION_90) + recentOrientedState2.setRecentsRotation(ROTATION_180) + assertNotEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + + recentOrientedState2.setRecentsRotation(ROTATION_90) + assertEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + } + + @Test + fun `stateId changes with display rotation`() { + val recentOrientedState1 = createRecentOrientedState() + val recentOrientedState2 = createRecentOrientedState() + recentOrientedState1.update(ROTATION_0, ROTATION_90) + recentOrientedState2.update(ROTATION_0, ROTATION_180) + assertNotEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + + recentOrientedState2.update(ROTATION_90, ROTATION_90) + assertNotEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + + recentOrientedState2.update(ROTATION_90, ROTATION_0) + assertNotEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + + recentOrientedState2.update(ROTATION_0, ROTATION_90) + assertEquals(recentOrientedState1.stateId, recentOrientedState2.stateId) + } + + @Test + fun `rotation handler test fixed landscape when device is portrait`() { + rotationHandlerTest( + testCase = + RotationHandlerTestCase( + displayRotation = INVALID_ROTATION, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + expectedHandler = PORTRAIT, + ) + } + + @Test + fun `rotation handler test fixed landscape when device is landscape`() { + rotationHandlerTest( + testCase = + RotationHandlerTestCase( + displayRotation = INVALID_ROTATION, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + expectedHandler = PORTRAIT, + ) + } + + @Test + fun `rotation handler test fixed landscape when device is seascape`() { + rotationHandlerTest( + testCase = + RotationHandlerTestCase( + displayRotation = INVALID_ROTATION, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + expectedHandler = PORTRAIT, + ) + } + + @Test + fun `rotation handler test fixed landscape when device is portrait and display rotation is portrait`() { + rotationHandlerTest( + testCase = + RotationHandlerTestCase( + displayRotation = ROTATION_0, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + expectedHandler = PORTRAIT, + ) + } + + @Test + fun `rotation handler test fixed landscape when device is landscape and display rotation is landscape `() { + rotationHandlerTest( + testCase = + RotationHandlerTestCase( + displayRotation = ROTATION_90, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + expectedHandler = PORTRAIT, + ) + } + + @Test + fun `rotation handler test fixed landscape when device is seascape and display rotation is seascape`() { + rotationHandlerTest( + testCase = + RotationHandlerTestCase( + displayRotation = ROTATION_180, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + expectedHandler = PORTRAIT, + ) + } + + @Test + fun `should hide actions fixed landscape no rotation`() { + shouldHideActionButtonTest( + testCase = + HideActionsTestCase( + recentsRotation = ROTATION_0, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + hideActionButtonsExpected = false, + ) + } + + @Test + fun `should hide actions fixed landscape rotation 90`() { + shouldHideActionButtonTest( + testCase = + HideActionsTestCase( + recentsRotation = ROTATION_90, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = true, + ), + hideActionButtonsExpected = false, + ) + } + + @Test + fun `should hide actions recent rotation 180`() { + shouldHideActionButtonTest( + testCase = + HideActionsTestCase( + recentsRotation = ROTATION_180, + touchRotation = ROTATION_0, + isRotationAllowed = false, + isFixedLandscape = false, + ), + hideActionButtonsExpected = true, + ) + } + + @Test + fun `should hide actions touch rotation 180`() { + shouldHideActionButtonTest( + testCase = + HideActionsTestCase( + recentsRotation = ROTATION_0, + touchRotation = ROTATION_180, + isRotationAllowed = false, + isFixedLandscape = false, + ), + hideActionButtonsExpected = true, + ) + } + + @Test + fun `should hide actions rotation allowed`() { + shouldHideActionButtonTest( + testCase = + HideActionsTestCase( + recentsRotation = ROTATION_90, + touchRotation = ROTATION_180, + isRotationAllowed = true, + isFixedLandscape = false, + ), + hideActionButtonsExpected = false, + ) + } + + @Test + fun `should hide actions fixed landscape rotations and not allowed`() { + shouldHideActionButtonTest( + testCase = + HideActionsTestCase( + recentsRotation = ROTATION_180, + touchRotation = ROTATION_90, + isRotationAllowed = false, + isFixedLandscape = true, + ), + hideActionButtonsExpected = false, + ) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentsOrientedStateTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentsOrientedStateTest.java deleted file mode 100644 index 47ef13b42f..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/RecentsOrientedStateTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.quickstep.util; - -import static android.view.Surface.ROTATION_0; -import static android.view.Surface.ROTATION_180; -import static android.view.Surface.ROTATION_90; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; - -import android.content.Context; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.quickstep.FallbackActivityInterface; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Tests for {@link RecentsOrientedState} - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class RecentsOrientedStateTest { - - private RecentsOrientedState mR1, mR2; - - @Before - public void setup() { - Context context = getApplicationContext(); - mR1 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { }); - mR2 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { }); - assertEquals(mR1.getStateId(), mR2.getStateId()); - } - - @Test - public void stateId_changesWithFlags() { - mR1.setGestureActive(true); - mR2.setGestureActive(false); - assertNotEquals(mR1.getStateId(), mR2.getStateId()); - - mR2.setGestureActive(true); - assertEquals(mR1.getStateId(), mR2.getStateId()); - } - - @Test - public void stateId_changesWithRecentsRotation() { - mR1.setRecentsRotation(ROTATION_90); - mR2.setRecentsRotation(ROTATION_180); - assertNotEquals(mR1.getStateId(), mR2.getStateId()); - - mR2.setRecentsRotation(ROTATION_90); - assertEquals(mR1.getStateId(), mR2.getStateId()); - } - - @Test - public void stateId_changesWithDisplayRotation() { - mR1.update(ROTATION_0, ROTATION_90); - mR2.update(ROTATION_0, ROTATION_180); - assertNotEquals(mR1.getStateId(), mR2.getStateId()); - - mR2.update(ROTATION_90, ROTATION_90); - assertNotEquals(mR1.getStateId(), mR2.getStateId()); - - mR2.update(ROTATION_90, ROTATION_0); - assertNotEquals(mR1.getStateId(), mR2.getStateId()); - - mR2.update(ROTATION_0, ROTATION_90); - assertEquals(mR1.getStateId(), mR2.getStateId()); - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt index d40f8ab389..c9d7e1dee9 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt @@ -27,15 +27,15 @@ import android.view.View import android.window.TransitionInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.apppairs.AppPairIcon +import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.views.GroupedTaskView import com.android.quickstep.views.IconView -import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskContainer import com.android.quickstep.views.TaskView -import com.android.quickstep.views.TaskView.TaskContainer import com.android.systemui.shared.recents.model.Task import org.junit.Assert.assertEquals import org.junit.Before @@ -59,7 +59,7 @@ class SplitAnimationControllerTest { private val mockSplitSelectStateController: SplitSelectStateController = mock() // TaskView private val mockTaskView: TaskView = mock() - private val mockThumbnailView: TaskThumbnailViewDeprecated = mock() + private val mockSnapshotView: View = mock() private val mockBitmap: Bitmap = mock() private val mockIconView: IconView = mock() private val mockTaskViewDrawable: Drawable = mock() @@ -77,6 +77,7 @@ class SplitAnimationControllerTest { private val splitSelectSource: SplitConfigurationOptions.SplitSelectSource = mock() private val mockSplitSourceDrawable: Drawable = mock() private val mockSplitSourceView: View = mock() + private val mockItemInfo: ItemInfo = mock() private val stateManager: StateManager<*, *> = mock() private val depthController: DepthController = mock() @@ -87,14 +88,17 @@ class SplitAnimationControllerTest { @Before fun setup() { - whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView) - whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap) + whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView) + whenever(mockTaskContainer.thumbnail).thenReturn(mockBitmap) whenever(mockTaskContainer.iconView).thenReturn(mockIconView) + whenever(mockTaskContainer.task).thenReturn(mockTask) whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable) whenever(mockTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer }) + whenever(mockTaskView.firstTaskContainer).thenReturn(mockTaskContainer) whenever(splitSelectSource.drawable).thenReturn(mockSplitSourceDrawable) whenever(splitSelectSource.view).thenReturn(mockSplitSourceView) + whenever(splitSelectSource.itemInfo).thenReturn(mockItemInfo) splitAnimationController = SplitAnimationController(mockSplitSelectStateController) } @@ -114,7 +118,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not fallback to use splitSource icon drawable", mockSplitSourceDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -130,7 +134,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not use taskView icon drawable", mockTaskViewDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -149,7 +153,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not use taskView icon drawable", mockTaskViewDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -165,7 +169,7 @@ class SplitAnimationControllerTest { assertEquals( "Did not use splitSource icon drawable", mockSplitSourceDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -180,7 +184,6 @@ class SplitAnimationControllerTest { whenever(mockTaskContainer.task).thenReturn(mockTask) whenever(mockTaskContainer.iconView).thenReturn(mockIconView) - whenever(mockTaskContainer.thumbnailViewDeprecated).thenReturn(mockThumbnailView) whenever(mockTask.getKey()).thenReturn(mockTaskKey) whenever(mockTaskKey.getId()).thenReturn(taskId) whenever(mockSplitSelectStateController.initialTaskId).thenReturn(taskId) @@ -188,13 +191,13 @@ class SplitAnimationControllerTest { val splitAnimInitProps: SplitAnimationController.Companion.SplitAnimInitProps = splitAnimationController.getFirstAnimInitViews( { mockGroupedTaskView }, - { splitSelectSource } + { splitSelectSource }, ) assertEquals( "Did not use splitSource icon drawable", mockSplitSourceDrawable, - splitAnimInitProps.iconDrawable + splitAnimInitProps.iconDrawable, ) } @@ -212,7 +215,7 @@ class SplitAnimationControllerTest { any(), any(), any(), - any() + any(), ) spySplitAnimationController.playSplitLaunchAnimation( @@ -227,7 +230,8 @@ class SplitAnimationControllerTest { depthController, null /* info */, null /* t */, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -240,7 +244,7 @@ class SplitAnimationControllerTest { any(), any(), any(), - any() + any(), ) } @@ -263,7 +267,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -276,7 +281,7 @@ class SplitAnimationControllerTest { whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper) doNothing() .whenever(spySplitAnimationController) - .composeIconSplitLaunchAnimator(any(), any(), any(), any()) + .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any()) doReturn(-1).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any()) spySplitAnimationController.playSplitLaunchAnimation( @@ -291,11 +296,12 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) - .composeIconSplitLaunchAnimator(any(), any(), any(), any()) + .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any()) } @Test @@ -319,7 +325,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -346,7 +353,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -373,7 +381,8 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) @@ -385,7 +394,7 @@ class SplitAnimationControllerTest { val spySplitAnimationController = spy(splitAnimationController) doNothing() .whenever(spySplitAnimationController) - .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) + .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, @@ -399,10 +408,11 @@ class SplitAnimationControllerTest { depthController, transitionInfo, transaction, - {} /* finishCallback */ + {} /* finishCallback */, + 1f, /* cornerRadius */ ) verify(spySplitAnimationController) - .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) + .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any(), any()) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt index bab84ef28b..d6f150c8e2 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -22,7 +22,6 @@ import android.app.PendingIntent import android.content.ComponentName import android.content.Intent import android.graphics.Rect -import android.os.Handler import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.LauncherState @@ -37,9 +36,11 @@ import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.RecentsModel import com.android.quickstep.SystemUiProxy import com.android.quickstep.util.SplitSelectStateController.SplitFromDesktopController +import com.android.quickstep.views.RecentsView import com.android.quickstep.views.RecentsViewContainer import com.android.systemui.shared.recents.model.Task -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50 +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import java.util.function.Consumer import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -52,6 +53,7 @@ import org.mockito.Mockito.any import org.mockito.Mockito.`when` import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -63,11 +65,11 @@ class SplitSelectStateControllerTest { private val statsLogManager: StatsLogManager = mock() private val statsLogger: StatsLogger = mock() private val stateManager: StateManager> = mock() - private val handler: Handler = mock() private val context: RecentsViewContainer = mock() private val recentsModel: RecentsModel = mock() private val pendingIntent: PendingIntent = mock() private val splitFromDesktopController: SplitFromDesktopController = mock() + private val recentsView: RecentsView<*, *> = mock() private lateinit var splitSelectStateController: SplitSelectStateController @@ -75,6 +77,7 @@ class SplitSelectStateControllerTest { private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10) private var taskIdCounter = 0 + private fun getUniqueId(): Int { return ++taskIdCounter } @@ -87,13 +90,12 @@ class SplitSelectStateControllerTest { splitSelectStateController = SplitSelectStateController( context, - handler, stateManager, depthController, statsLogManager, systemUiProxy, recentsModel, - null /*activityBackCallback*/ + null, /*activityBackCallback*/ ) } @@ -101,14 +103,14 @@ class SplitSelectStateControllerTest { fun activeTasks_noMatchingTasks() { val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName("pumpkin", "pie") + ComponentName("pumpkin", "pie"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("hotdog", "juice"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -125,7 +127,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -142,14 +144,14 @@ class SplitSelectStateControllerTest { val matchingComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pomegranate", "juice") + ComponentName("pomegranate", "juice"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pumpkin", "pie"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -162,14 +164,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -178,7 +180,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -195,14 +197,14 @@ class SplitSelectStateControllerTest { val nonPrimaryUserComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pomegranate", "juice") + ComponentName("pomegranate", "juice"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pumpkin", "pie"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -219,7 +221,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -236,16 +238,16 @@ class SplitSelectStateControllerTest { val nonPrimaryUserComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle, ComponentName("pomegranate", "juice"), - nonPrimaryUserHandle + nonPrimaryUserHandle, ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pumpkin", "pie"), - ComponentName("personal", "computer") + ComponentName("personal", "computer"), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask1) @@ -258,15 +260,15 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -275,7 +277,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonPrimaryUserComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -292,14 +294,14 @@ class SplitSelectStateControllerTest { val matchingComponent = ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pumpkin", "pie") + ComponentName("pumpkin", "pie"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -312,14 +314,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -328,7 +330,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -347,11 +349,11 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) + generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -366,14 +368,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[1].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[1].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[1], groupTask2.task2) + assertEquals(it[1], groupTask2.bottomRightTask) } // Capture callback from recentsModel#getTasks() @@ -382,7 +384,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(nonMatchingComponent, matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -400,11 +402,11 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) + generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -418,14 +420,14 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask2.task2) + assertEquals(it[0], groupTask2.bottomRightTask) assertNull("No tasks should have matched", it[1] /*task*/) } @@ -435,7 +437,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -453,14 +455,14 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage, matchingClass), - ComponentName("pumpkin", "pie") + ComponentName("pumpkin", "pie"), ) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName("pomegranate", "juice"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask2) @@ -474,25 +476,25 @@ class SplitSelectStateControllerTest { assertEquals( "ComponentName package mismatched", it[0].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[0].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[0], groupTask1.task1) + assertEquals(it[0], groupTask1.topLeftTask) assertEquals( "ComponentName package mismatched", it[1].key.baseIntent.component?.packageName, - matchingPackage + matchingPackage, ) assertEquals( "ComponentName class mismatched", it[1].key.baseIntent.component?.className, - matchingClass + matchingClass, ) - assertEquals(it[1], groupTask2.task2) + assertEquals(it[1], groupTask2.bottomRightTask) } // Capture callback from recentsModel#getTasks() @@ -501,7 +503,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent, matchingComponent), false /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -523,16 +525,16 @@ class SplitSelectStateControllerTest { ComponentKey(ComponentName(matchingPackage2, matchingClass2), primaryUserHandle) val groupTask1 = - generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) + generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie")) val groupTask2 = - generateGroupTask( + generateSplitTask( ComponentName(matchingPackage2, matchingClass2), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val groupTask3 = - generateGroupTask( + generateSplitTask( ComponentName("hotdog", "pie"), - ComponentName(matchingPackage, matchingClass) + ComponentName(matchingPackage, matchingClass), ) val tasks: ArrayList = ArrayList() tasks.add(groupTask3) @@ -544,7 +546,7 @@ class SplitSelectStateControllerTest { val taskConsumer = Consumer> { assertEquals("Expected array length 2", 2, it.size) - assertEquals("Found wrong task", it[0], groupTask2.task1) + assertEquals("Found wrong task", it[0], groupTask2.topLeftTask) } // Capture callback from recentsModel#getTasks() @@ -553,7 +555,7 @@ class SplitSelectStateControllerTest { splitSelectStateController.findLastActiveTasksAndRunCallback( listOf(matchingComponent2, matchingComponent), true /* findExactPairMatch */, - taskConsumer + taskConsumer, ) verify(recentsModel).getTasks(capture()) } @@ -570,7 +572,7 @@ class SplitSelectStateControllerTest { -1 /*stagePosition*/, ItemInfo(), null /*splitEvent*/, - 10 /*alreadyRunningTask*/ + 10, /*alreadyRunningTask*/ ) assertTrue(splitSelectStateController.isSplitSelectActive) } @@ -582,21 +584,23 @@ class SplitSelectStateControllerTest { -1 /*stagePosition*/, ItemInfo(), null /*splitEvent*/, - -1 /*alreadyRunningTask*/ + -1, /*alreadyRunningTask*/ ) assertTrue(splitSelectStateController.isSplitSelectActive) } @Test fun resetAfterInitial() { + whenever(context.getOverviewPanel>()).thenReturn(recentsView) splitSelectStateController.setInitialTaskSelect( Intent() /*intent*/, -1 /*stagePosition*/, ItemInfo(), null /*splitEvent*/, - -1 + -1, ) splitSelectStateController.resetState() + verify(recentsView, times(1)).resetDesktopTaskFromSplitSelectState() assertFalse(splitSelectStateController.isSplitSelectActive) } @@ -622,11 +626,26 @@ class SplitSelectStateControllerTest { verify(splitFromDesktopController).onDestroy() } - // Generate GroupTask with default userId. - private fun generateGroupTask( + @Test + fun splitSelectStateControllerDestroyed_doNotResetDeskTopTasks() { + whenever(context.getOverviewPanel>()).thenReturn(recentsView) + splitSelectStateController.setInitialTaskSelect( + Intent(), /*intent*/ + -1, /*stagePosition*/ + ItemInfo(), + null, /*splitEvent*/ + -1, + ) + splitSelectStateController.onDestroy() + splitSelectStateController.resetState() + verify(recentsView, times(0)).resetDesktopTaskFromSplitSelectState() + } + + /** Generates a [SplitTask] with default userId. */ + private fun generateSplitTask( task1ComponentName: ComponentName, - task2ComponentName: ComponentName - ): GroupTask { + task2ComponentName: ComponentName, + ): SplitTask { val task1 = Task() var taskInfo = ActivityManager.RunningTaskInfo() taskInfo.taskId = getUniqueId() @@ -642,20 +661,26 @@ class SplitSelectStateControllerTest { intent.component = task2ComponentName taskInfo.baseIntent = intent task2.key = Task.TaskKey(taskInfo) - return GroupTask( + return SplitTask( task1, task2, - SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_50_50) + SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ task1.key.id, + /* rightBottomTaskId = */ task2.key.id, + /* snapPosition = */ SNAP_TO_2_50_50, + ), ) } - // Generate GroupTask with custom user handles. - private fun generateGroupTask( + /** Generates a [SplitTask] with custom user handles. */ + private fun generateSplitTask( task1ComponentName: ComponentName, userHandle1: UserHandle, task2ComponentName: ComponentName, - userHandle2: UserHandle - ): GroupTask { + userHandle2: UserHandle, + ): SplitTask { val task1 = Task() var taskInfo = ActivityManager.RunningTaskInfo() taskInfo.taskId = getUniqueId() @@ -674,10 +699,16 @@ class SplitSelectStateControllerTest { intent.component = task2ComponentName taskInfo.baseIntent = intent task2.key = Task.TaskKey(taskInfo) - return GroupTask( + return SplitTask( task1, task2, - SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_50_50) + SplitBounds( + /* leftTopBounds = */ Rect(), + /* rightBottomBounds = */ Rect(), + /* leftTopTaskId = */ task1.key.id, + /* rightBottomTaskId = */ task2.key.id, + /* snapPosition = */ SNAP_TO_2_50_50, + ), ) } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java deleted file mode 100644 index 7ef4910ce5..0000000000 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.java +++ /dev/null @@ -1,510 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep.util; - -import static com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID; -import static com.android.quickstep.util.TaskGridNavHelper.INVALID_FOCUSED_TASK_ID; - -import static org.junit.Assert.assertEquals; - -import com.android.launcher3.util.IntArray; - -import org.junit.Test; - -public class TaskGridNavHelperTest { - - @Test - public void equalLengthRows_noFocused_onTop_pressDown_goesToBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressUp_goesToBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressDown_goesToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressUp_goesToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressLeft_goesLeft() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 3, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 4, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 3; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 4; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 5; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 6; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 6, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressLeft_toTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = focusedTaskId; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage); - } - - @Test - public void equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int focusedTaskId = 99; - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, focusedTaskId); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 7; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 7; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 6; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerTopRow_noFocused_atClearAll_pressRight_goToLonger() { - IntArray topIds = IntArray.wrap(1, 3, 5, 7); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6, 7); - int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 7, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressTab_goesToBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 1; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = 1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 3, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 3; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 2, nextGridPage); - } - - @Test - public void equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() { - IntArray topIds = IntArray.wrap(1, 3, 5); - IntArray bottomIds = IntArray.wrap(2, 4, 6); - int currentPageTaskViewId = 2; - int delta = -1; - @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB; - boolean cycle = true; - TaskGridNavHelper taskGridNavHelper = - new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID); - - int nextGridPage = - taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle); - - assertEquals("Wrong next page returned.", 1, nextGridPage); - } -} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt new file mode 100644 index 0000000000..f776b94cd0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt @@ -0,0 +1,869 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util + +import com.android.launcher3.util.IntArray +import com.android.quickstep.util.TaskGridNavHelper.Companion.ADD_DESK_PLACEHOLDER_ID +import com.android.quickstep.util.TaskGridNavHelper.Companion.CLEAR_ALL_PLACEHOLDER_ID +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.DOWN +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.LEFT +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.RIGHT +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.TAB +import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.UP +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class TaskGridNavHelperTest { + + /* + 5 3 1 + CLEAR_ALL ↓ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressDown_goesToBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, DOWN, delta = 1)).isEqualTo(2) + } + + /* ↑----→ + 5 3 1 | + CLEAR_ALL | + 6 4 2←---| + */ + @Test + fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, UP, delta = 1)).isEqualTo(2) + } + + /* ↓----↑ + 5 3 1 | + CLEAR_ALL | + 6 4 2 | + ↓----→ + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, DOWN, delta = 1)).isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, UP, delta = 1)).isEqualTo(1) + } + + /* + 5 3<--1 + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, LEFT, delta = 1)).isEqualTo(3) + } + + /* + 5 3 1 + CLEAR_ALL + 6 4<--2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, LEFT, delta = 1)).isEqualTo(4) + } + + /* + 5 3-->1 + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() { + assertThat(getNextGridPage(currentPageTaskViewId = 3, RIGHT, delta = -1)).isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL + 6 4-->2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() { + assertThat(getNextGridPage(currentPageTaskViewId = 4, RIGHT, delta = -1)).isEqualTo(2) + } + + /* + ↓------------------← + | | + ↓ 5 3 1---→ + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, RIGHT, delta = -1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + ↓------------------← + | ↑ + ↓ 5 3 1 | + CLEAR_ALL ↑ + 6 4 2---→ + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, RIGHT, delta = -1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + ←----5 3 1 + ↓ + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 5, LEFT, delta = 1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL + ↑ + ←---6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() { + assertThat(getNextGridPage(currentPageTaskViewId = 6, LEFT, delta = 1)) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→-----------------------| + | ↓ + ↑ 5 3 1 + ←------CLEAR_ALL + + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() { + assertThat( + getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, LEFT, delta = 1) + ) + .isEqualTo(1) + } + + /* + 5 3 1 + CLEAR_ALL--↓ + | + |--→6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() { + assertThat( + getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, RIGHT, delta = -1) + ) + .isEqualTo(6) + } + + /* + 5 3 1←--- + ↑ + CLEAR_ALL ←--FOCUSED_TASK + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onFocused_pressLeft_toTop() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + LEFT, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(1) + } + + /* + 5 3 1 + ←--↑ + CLEAR_ALL ↓-→FOCUSED_TASK + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + UP, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL ↑--→FOCUSED_TASK + ↑←--↓ + 6 4 2 + */ + + @Test + fun equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() { + + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + DOWN, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + ↓-------------------------------←| + | ↑ + ↓ 5 3 1 | + CLEAR_ALL FOCUSED_TASK--→ + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() { + + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→---------------------------| + | | + ↑ 5 3 1 ↓ + ←------CLEAR_ALL FOCUSED_TASK + + 6 4 2 + */ + @Test + fun equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() { + + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + LEFT, + delta = 1, + largeTileIds = listOf(FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 7←-↑ 5 3 1 + ↓--→ + CLEAR_ALL + 6 4 2 + */ + @Test + fun longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 7, + DOWN, + delta = 1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + ←--↑ + ↓-→7 5 3 1 + CLEAR_ALL + 6 4 2 + */ + @Test + fun longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() { + assertThat( + getNextGridPage( + /* topIds = */ currentPageTaskViewId = 7, + UP, + delta = 1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + 7 5 3 1 + CLEAR_ALL ↑ + ←----6 4 2 + */ + @Test + fun longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() { + assertThat( + getNextGridPage( + /* topIds = */ currentPageTaskViewId = 6, + LEFT, + delta = 1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + 7 5 3 1 + ↑ + CLEAR_ALL-----→ + 6 4 2 + */ + @Test + fun longerTopRow_noFocused_atClearAll_pressRight_goToLonger() { + assertThat( + getNextGridPage( + /* topIds = */ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + RIGHT, + delta = -1, + topIds = IntArray.wrap(1, 3, 5, 7), + ) + ) + .isEqualTo(7) + } + + /* + 5 3 1 + CLEAR_ALL-----→ + ↓ + 7 6 4 2 + */ + @Test + fun longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + RIGHT, + delta = -1, + bottomIds = IntArray.wrap(2, 4, 6, 7), + ) + ) + .isEqualTo(7) + } + + /* + 5 3 1 + CLEAR_ALL ↓ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressTab_goesToBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = 1)).isEqualTo(2) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + ←---↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = 1)).isEqualTo(3) + } + + /* + 5 3 1 + CLEAR_ALL ↓ + ----→ + ↓ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() { + assertThat(getNextGridPage(currentPageTaskViewId = 3, TAB, delta = -1)).isEqualTo(2) + } + + /* + 5 3 1 + CLEAR_ALL ↑ + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = -1)).isEqualTo(1) + } + + /* + 5 3 [1] + CLEAR_ALL + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() { + assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = -1, cycle = false)) + .isEqualTo(1) + } + + /* + 5 3 1 + [CLEAR_ALL] + 6 4 2 + */ + @Test + fun equalLengthRows_noFocused_onClearAll_pressTab_noCycle_staysOnClearAll() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + TAB, + delta = 1, + cycle = false, + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL FOCUSED_TASK←--DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressLeftFromDesktopTask_goesToFocusedTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + LEFT, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL FOCUSED_TASK--→DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressRightFromFocusedTask_goesToDesktopTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(DESKTOP_TASK_ID) + } + + /* + ↓-----------------------------------------←| + | | + ↓ 5 3 1 ↑ + CLEAR_ALL FOCUSED_TASK DESKTOP--→ + 6 4 2 + */ + @Test + fun withLargeTile_pressRightFromDesktopTask_goesToClearAll() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→-------------------------------------------| + | | + ↑ 5 3 1 ↓ + ←------CLEAR_ALL FOCUSED_TASK DESKTOP + + 6 4 2 + */ + @Test + fun withLargeTile_pressLeftFromClearAll_goesToDesktopTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + LEFT, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(DESKTOP_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL FOCUSED_TASK DESKTOP + ↑ + 6 4 2→----↑ + */ + @Test + fun withLargeTile_pressRightFromBottom_goesToLargeTile() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 2, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1→----| + ↓ + CLEAR_ALL FOCUSED_TASK DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressRightFromTop_goesToLargeTile() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 1, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + 5 3 1 + + CLEAR_ALL FOCUSED_TASK←---DESKTOP + 6 4 2 + */ + @Test + fun withLargeTile_pressTabFromDeskTop_goesToFocusedTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + TAB, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(FOCUSED_TASK_ID) + } + + /* + CLEAR_ALL FOCUSED_TASK DESKTOP + ↓ + 2←----↓ + */ + @Test + fun withLargeTile_pressLeftFromLargeTile_goesToBottom() { + assertThat( + getNextGridPage( + currentPageTaskViewId = FOCUSED_TASK_ID, + LEFT, + delta = 1, + topIds = IntArray(), + bottomIds = IntArray.wrap(2), + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(2) + } + + /* + ↓-----------------------------------------←| + | | + ↓ 5 3 1 ↑ + CLEAR_ALL FOCUSED_TASK DESKTOP--→ + 6 4 2 + */ + @Test + fun withLargeTile_pressShiftTabFromDeskTop_goesToClearAll() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + TAB, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID), + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + 5 3 1→----| + ↓ + CLEAR_ALL ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressRightFromTop_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 1, + RIGHT, + delta = -1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL ADD_DESKTOP + ↑ + 6 4 2→----↑ + */ + @Test + fun withAddDesktopButton_pressRightFromBottom_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = 2, + RIGHT, + delta = -1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + ↓-------------------------------←| + | ↑ + ↓ 5 3 1 | + CLEAR_ALL ADD_DESKTOP--→ + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressRightFromAddDesktopButton_goesToClearAllButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + RIGHT, + delta = -1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID) + } + + /* + |→--------------------------------| + | | + ↑ 5 3 1 ↓ + ←------CLEAR_ALL ADD_DESKTOP + + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressLeftFromClearAllButton_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, + LEFT, + delta = 1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + ←--↑ + CLEAR_ALL ↓-→ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressUpOnAddDesktop_stayOnAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + UP, + delta = 1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL ↑--→ADD_DESKTOP + ↑←--↓ + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressDownOnAddDesktop_stayOnAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + DOWN, + delta = 1, + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 + CLEAR_ALL DESKTOP--→ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressRightFromDesktopTask_goesToAddDesktopButton() { + assertThat( + getNextGridPage( + currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID, + LEFT, + delta = 1, + largeTileIds = listOf(DESKTOP_TASK_ID), + hasAddDesktopButton = true, + ) + ) + .isEqualTo(DESKTOP_TASK_ID) + } + + /* + 5 3 1 + CLEAR_ALL DESKTOP←--ADD_DESKTOP + 6 4 2 + */ + @Test + fun withAddDesktopButton_pressLeftFromAddDesktopButton_goesToDesktopTask() { + assertThat( + getNextGridPage( + currentPageTaskViewId = DESKTOP_TASK_ID, + RIGHT, + delta = -1, + largeTileIds = listOf(DESKTOP_TASK_ID), + hasAddDesktopButton = true, + ) + ) + .isEqualTo(ADD_DESK_PLACEHOLDER_ID) + } + + /* + 5 3 1 | + CLEAR_ALL | Invalid ID: + 6 4 2 | [25] --> [25] + */ + @Test + fun nextGridPage_invalidId_pressTab_noCycle_returnsCurrentPage() { + assertThat(getNextGridPage(currentPageTaskViewId = 25, TAB, delta = -1, cycle = false)) + .isEqualTo(25) + } + + // Col offset: 0 1 2 + // ----------- + // ID grid: 4 2 0 start + // end [5] 3 1 + @Test + fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsStart() { + val expected = listOf(Pair(4, 0), Pair(3, 1), Pair(2, 1), Pair(1, 2), Pair(0, 2)) + assertThat( + gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId = 5, + towardsStart = true, + ) + .toList() + ) + .isEqualTo(expected) + } + + // Col offset: 2 1 0 + // ----------- + // ID grid: 4 2 [0] start + // end 5 3 1 + @Test + fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsEnd() { + val expected = listOf(Pair(1, 0), Pair(2, 1), Pair(3, 1), Pair(4, 2), Pair(5, 2)) + assertThat( + gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId = 0, + towardsStart = false, + ) + .toList() + ) + .isEqualTo(expected) + } + + private fun getNextGridPage( + currentPageTaskViewId: Int, + direction: TaskGridNavHelper.TaskNavDirection, + delta: Int, + topIds: IntArray = IntArray.wrap(1, 3, 5), + bottomIds: IntArray = IntArray.wrap(2, 4, 6), + largeTileIds: List = emptyList(), + hasAddDesktopButton: Boolean = false, + cycle: Boolean = true, + ): Int { + val taskGridNavHelper = + TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton) + return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle) + } + + private fun gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId: Int, + towardsStart: Boolean, + topIds: IntArray = IntArray.wrap(0, 2, 4), + bottomIds: IntArray = IntArray.wrap(1, 3, 5), + largeTileIds: List = emptyList(), + hasAddDesktopButton: Boolean = false, + ): Sequence> { + val taskGridNavHelper = + TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton) + return taskGridNavHelper.gridTaskViewIdOffsetPairInTabOrderSequence( + initialTaskViewId, + towardsStart, + ) + } + + private companion object { + const val FOCUSED_TASK_ID = 99 + const val DESKTOP_TASK_ID = 100 + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java index 72cfd92b86..060f13f1e2 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java @@ -35,20 +35,27 @@ import androidx.test.filters.SmallTest; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.util.AllModulesMinusWMProxy; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.Info; -import com.android.launcher3.util.LauncherModelHelper; import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.RotationUtils; +import com.android.launcher3.util.SandboxApplication; import com.android.launcher3.util.WindowBounds; import com.android.launcher3.util.window.CachedDisplayInfo; import com.android.launcher3.util.window.WindowManagerProxy; import com.android.quickstep.FallbackActivityInterface; import com.android.quickstep.util.SurfaceTransaction.MockProperties; +import dagger.BindsInstance; +import dagger.Component; + import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,6 +66,8 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class TaskViewSimulatorTest { + @Rule public final SandboxApplication app = new SandboxApplication(); + @Test public void taskProperlyScaled_portrait_noRotation_sameInsets1() { new TaskMatrixVerifier() @@ -117,7 +126,7 @@ public class TaskViewSimulatorTest { .verifyNoTransforms(); } - private static class TaskMatrixVerifier extends TransformParams { + private class TaskMatrixVerifier extends TransformParams { private Point mDisplaySize = new Point(); private int mDensityDpi = DisplayMetrics.DENSITY_DEFAULT; @@ -157,71 +166,67 @@ public class TaskViewSimulatorTest { } void verifyNoTransforms() { - LauncherModelHelper helper = new LauncherModelHelper(); - try { - int rotation = mDisplaySize.x > mDisplaySize.y - ? Surface.ROTATION_90 : Surface.ROTATION_0; - CachedDisplayInfo cdi = new CachedDisplayInfo(mDisplaySize, rotation); - WindowBounds wm = new WindowBounds( - new Rect(0, 0, mDisplaySize.x, mDisplaySize.y), - mDisplayInsets); - List allBounds = new ArrayList<>(4); - for (int i = 0; i < 4; i++) { - Rect boundsR = new Rect(wm.bounds); - Rect insetsR = new Rect(wm.insets); + DisplayController mockController = mock(DisplayController.class); - RotationUtils.rotateRect(insetsR, RotationUtils.deltaRotation(rotation, i)); - RotationUtils.rotateRect(boundsR, RotationUtils.deltaRotation(rotation, i)); - boundsR.set(0, 0, Math.abs(boundsR.width()), Math.abs(boundsR.height())); - allBounds.add(new WindowBounds(boundsR, insetsR)); - } + app.initDaggerComponent( + DaggerTaskViewSimulatorTest_TaskViewSimulatorTestComponent.builder() + .bindDisplayController(mockController)); + int rotation = mDisplaySize.x > mDisplaySize.y + ? Surface.ROTATION_90 : Surface.ROTATION_0; + CachedDisplayInfo cdi = new CachedDisplayInfo(mDisplaySize, rotation); + WindowBounds wm = new WindowBounds( + new Rect(0, 0, mDisplaySize.x, mDisplaySize.y), + mDisplayInsets); + List allBounds = new ArrayList<>(4); + for (int i = 0; i < 4; i++) { + Rect boundsR = new Rect(wm.bounds); + Rect insetsR = new Rect(wm.insets); - WindowManagerProxy wmProxy = mock(WindowManagerProxy.class); - doReturn(cdi).when(wmProxy).getDisplayInfo(any()); - doReturn(wm).when(wmProxy).getRealBounds(any(), any()); - doReturn(NavigationMode.NO_BUTTON).when(wmProxy).getNavigationMode(any()); - - ArrayMap> perDisplayBoundsCache = - new ArrayMap<>(); - perDisplayBoundsCache.put(cdi.normalize(wmProxy), allBounds); - - Configuration configuration = new Configuration(); - configuration.densityDpi = mDensityDpi; - Context configurationContext = helper.sandboxContext.createConfigurationContext( - configuration); - - DisplayController.Info info = new Info( - configurationContext, wmProxy, perDisplayBoundsCache); - - DisplayController mockController = mock(DisplayController.class); - when(mockController.getInfo()).thenReturn(info); - helper.sandboxContext.putObject(DisplayController.INSTANCE, mockController); - - mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(helper.sandboxContext) - .getBestMatch(mAppBounds.width(), mAppBounds.height(), rotation); - mDeviceProfile.updateInsets(mLauncherInsets); - - TaskViewSimulator tvs = new TaskViewSimulator(helper.sandboxContext, - FallbackActivityInterface.INSTANCE); - tvs.setDp(mDeviceProfile); - - int launcherRotation = info.rotation; - if (mAppRotation < 0) { - mAppRotation = launcherRotation; - } - - tvs.getOrientationState().update(launcherRotation, mAppRotation); - if (mAppInsets == null) { - mAppInsets = new Rect(mLauncherInsets); - } - tvs.setPreviewBounds(mAppBounds, mAppInsets); - - tvs.fullScreenProgress.value = 1; - tvs.recentsViewScale.value = tvs.getFullScreenScale(); - tvs.apply(this); - } finally { - helper.destroy(); + RotationUtils.rotateRect(insetsR, RotationUtils.deltaRotation(rotation, i)); + RotationUtils.rotateRect(boundsR, RotationUtils.deltaRotation(rotation, i)); + boundsR.set(0, 0, Math.abs(boundsR.width()), Math.abs(boundsR.height())); + allBounds.add(new WindowBounds(boundsR, insetsR)); } + + WindowManagerProxy wmProxy = mock(WindowManagerProxy.class); + doReturn(cdi).when(wmProxy).getDisplayInfo(any()); + doReturn(wm).when(wmProxy).getRealBounds(any(), any()); + doReturn(NavigationMode.NO_BUTTON).when(wmProxy).getNavigationMode(any()); + + ArrayMap> perDisplayBoundsCache = + new ArrayMap<>(); + perDisplayBoundsCache.put(cdi.normalize(wmProxy), allBounds); + + Configuration configuration = new Configuration(); + configuration.densityDpi = mDensityDpi; + Context configurationContext = app.createConfigurationContext(configuration); + + DisplayController.Info info = new Info( + configurationContext, false, wmProxy, perDisplayBoundsCache, mDensityDpi); + when(mockController.getInfo()).thenReturn(info); + + mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(app) + .getBestMatch(mAppBounds.width(), mAppBounds.height(), rotation); + mDeviceProfile.updateInsets(mLauncherInsets); + + TaskViewSimulator tvs = new TaskViewSimulator(app, + FallbackActivityInterface.INSTANCE, false, 0); + tvs.setDp(mDeviceProfile); + + int launcherRotation = info.rotation; + if (mAppRotation < 0) { + mAppRotation = launcherRotation; + } + + tvs.getOrientationState().update(launcherRotation, mAppRotation); + if (mAppInsets == null) { + mAppInsets = new Rect(mLauncherInsets); + } + tvs.setPreviewBounds(mAppBounds, mAppInsets); + + tvs.fullScreenProgress.value = 1; + tvs.recentsViewScale.value = tvs.getFullScreenScale(); + tvs.apply(this); } @Override @@ -271,4 +276,18 @@ public class TaskViewSimulatorTest { description.appendValue(mExpected); } } + + @LauncherAppSingleton + @Component(modules = {AllModulesMinusWMProxy.class}) + interface TaskViewSimulatorTestComponent extends LauncherAppComponent { + + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + + @BindsInstance + Builder bindDisplayController(DisplayController controller); + + TaskViewSimulatorTestComponent build(); + } + } } diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt new file mode 100644 index 0000000000..6c526a4855 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TestExtensions.kt @@ -0,0 +1,65 @@ +/* + * 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.quickstep.util + +import com.android.launcher3.BuildConfig +import com.android.launcher3.util.SafeCloseable +import com.android.quickstep.DeviceConfigWrapper.Companion.configHelper +import com.android.quickstep.util.DeviceConfigHelper.Companion.prefs +import java.util.concurrent.CountDownLatch +import java.util.function.BooleanSupplier +import org.junit.Assert +import org.junit.Assume + +/** Helper methods for testing */ +object TestExtensions { + + @JvmStatic + fun overrideNavConfigFlag( + key: String, + value: Boolean, + targetValue: BooleanSupplier + ): AutoCloseable { + Assume.assumeTrue(BuildConfig.IS_DEBUG_DEVICE) + if (targetValue.asBoolean == value) { + return AutoCloseable {} + } + + navConfigEditWatcher().let { + prefs.edit().putBoolean(key, value).commit() + it.close() + } + Assert.assertEquals(value, targetValue.asBoolean) + + val watcher = navConfigEditWatcher() + return AutoCloseable { + prefs.edit().remove(key).commit() + watcher.close() + } + } + + private fun navConfigEditWatcher(): SafeCloseable { + val wait = CountDownLatch(1) + val listener = Runnable { wait.countDown() } + configHelper.addChangeListener(listener) + + return SafeCloseable { + wait.await() + configHelper.removeChangeListener(listener) + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt new file mode 100644 index 0000000000..6dbb667a22 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_OPEN +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.TransitionInfo.FLAG_NONE +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.quickstep.RemoteAnimationTargets +import com.android.quickstep.util.TransformParams.BuilderProxy.NO_OP +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TransformParamsTest { + private val surfaceTransaction = mock() + private val transaction = mock() + private val transformParams = TransformParams(::surfaceTransaction) + + private val freeformTaskInfo1 = + createTaskInfo(taskId = 1, windowingMode = WINDOWING_MODE_FREEFORM) + private val freeformTaskInfo2 = + createTaskInfo(taskId = 2, windowingMode = WINDOWING_MODE_FREEFORM) + private val fullscreenTaskInfo1 = + createTaskInfo(taskId = 1, windowingMode = WINDOWING_MODE_FULLSCREEN) + + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() + + @Before + fun setUp() { + whenever(surfaceTransaction.transaction).thenReturn(transaction) + whenever(surfaceTransaction.forSurface(anyOrNull())) + .thenReturn(mock()) + transformParams.setCornerRadius(CORNER_RADIUS) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_freeformTasks_overridesCornerRadius() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash1 = mock() + val leash2 = mock() + transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1)) + transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2))) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction).setCornerRadius(leash1, 0f) + verify(transaction).setCornerRadius(leash2, 0f) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_freeformTasks_overridesCornerRadiusOnlyOnce() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash1 = mock() + val leash2 = mock() + transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1)) + transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2))) + transformParams.createSurfaceParams(NO_OP) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction).setCornerRadius(leash1, 0f) + verify(transaction).setCornerRadius(leash2, 0f) + } + + @Test + @DisableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_flagDisabled_doesntOverrideCornerRadius() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash1 = mock() + val leash2 = mock() + transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1)) + transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2))) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction, never()).setCornerRadius(leash1, 0f) + verify(transaction, never()).setCornerRadius(leash2, 0f) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun createSurfaceParams_fullscreenTasks_doesntOverrideCornerRadius() { + val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE) + val leash = mock() + transitionInfo.addChange(createChange(fullscreenTaskInfo1, leash = leash)) + transformParams.setTransitionInfo(transitionInfo) + transformParams.setTargetSet(createTargetSet(listOf(fullscreenTaskInfo1))) + + transformParams.createSurfaceParams(NO_OP) + + verify(transaction, never()).setCornerRadius(leash, 0f) + } + + private fun createTargetSet(taskInfos: List): RemoteAnimationTargets { + val remoteAnimationTargets = mutableListOf() + taskInfos.map { remoteAnimationTargets.add(createRemoteAnimationTarget(it)) } + return RemoteAnimationTargets( + remoteAnimationTargets.toTypedArray(), + /* wallpapers= */ null, + /* nonApps= */ null, + /* targetMode= */ TRANSIT_OPEN, + ) + } + + private fun createRemoteAnimationTarget(taskInfo: RunningTaskInfo): RemoteAnimationTarget { + val windowConfig = mock() + whenever(windowConfig.activityType).thenReturn(ACTIVITY_TYPE_STANDARD) + return RemoteAnimationTarget( + taskInfo.taskId, + /* mode= */ TRANSIT_OPEN, + /* leash= */ null, + /* isTranslucent= */ false, + /* clipRect= */ null, + /* contentInsets= */ null, + /* prefixOrderIndex= */ 0, + /* position= */ null, + /* localBounds= */ null, + /* screenSpaceBounds= */ null, + windowConfig, + /* isNotInRecents= */ false, + /* startLeash= */ null, + /* startBounds= */ null, + taskInfo, + /* allowEnterPip= */ false, + ) + } + + private fun createTaskInfo(taskId: Int, windowingMode: Int): RunningTaskInfo { + val taskInfo = RunningTaskInfo() + taskInfo.taskId = taskId + taskInfo.configuration.windowConfiguration.windowingMode = windowingMode + return taskInfo + } + + private fun createChange(taskInfo: RunningTaskInfo, leash: SurfaceControl): Change { + val taskInfo = createTaskInfo(taskInfo.taskId, taskInfo.windowingMode) + val change = Change(taskInfo.token, mock()) + change.mode = TRANSIT_OPEN + change.taskInfo = taskInfo + change.leash = leash + return change + } + + private companion object { + private const val CORNER_RADIUS = 30f + } +} diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java index 7b57c81b74..69499fbb76 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -20,6 +20,7 @@ import static android.content.pm.ApplicationInfo.FLAG_INSTALLED; import static android.os.Process.myUserHandle; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; +import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; @@ -41,49 +42,64 @@ import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; +import android.os.Process; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.text.TextUtils; -import androidx.test.core.content.pm.ApplicationInfoBuilder; +import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.Flags; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; -import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.util.LauncherLayoutBuilder; -import com.android.launcher3.util.LauncherModelHelper; +import com.android.launcher3.util.ModelTestExtensions; +import com.android.launcher3.util.SandboxApplication; +import com.android.launcher3.util.rule.LayoutProviderRule; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.PendingAddWidgetInfo; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @SmallTest @RunWith(AndroidJUnit4.class) public final class WidgetsPredicationUpdateTaskTest { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule public SandboxApplication mContext = new SandboxApplication().withModelDependency(); + @Rule public LayoutProviderRule mLayoutProvider = new LayoutProviderRule(mContext); + + private AppWidgetProviderInfo mApp1Provider1; private AppWidgetProviderInfo mApp1Provider2; private AppWidgetProviderInfo mApp2Provider1; private AppWidgetProviderInfo mApp4Provider1; private AppWidgetProviderInfo mApp4Provider2; private AppWidgetProviderInfo mApp5Provider1; + private AppWidgetProviderInfo mApp6PinOnlyProvider1; private List allWidgets; private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback(); - private LauncherModelHelper mModelHelper; private UserHandle mUserHandle; private LauncherApps mLauncherApps; @@ -91,7 +107,6 @@ public final class WidgetsPredicationUpdateTaskTest { @Before public void setup() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS); - mModelHelper = new LauncherModelHelper(); mUserHandle = myUserHandle(); mApp1Provider1 = createAppWidgetProviderInfo( @@ -106,22 +121,28 @@ public final class WidgetsPredicationUpdateTaskTest { ComponentName.createRelative("app4", ".provider2")); mApp5Provider1 = createAppWidgetProviderInfo( ComponentName.createRelative("app5", "provider1")); - allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, - mApp4Provider1, mApp4Provider2, mApp5Provider1); + mApp6PinOnlyProvider1 = createAppWidgetProviderInfo( + ComponentName.createRelative("app6", "provider1"), + /*hideFromPicker=*/ true + ); - mLauncherApps = mModelHelper.sandboxContext.spyService(LauncherApps.class); + + allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, + mApp4Provider1, mApp4Provider2, mApp5Provider1, mApp6PinOnlyProvider1); + + mLauncherApps = mContext.spyService(LauncherApps.class); doAnswer(i -> { String pkg = i.getArgument(0); - ApplicationInfo applicationInfo = ApplicationInfoBuilder.newBuilder() - .setPackageName(pkg) - .setName("App " + pkg) - .build(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = pkg; + applicationInfo.name = "App " + pkg; + applicationInfo.uid = Process.myUid(); applicationInfo.category = CATEGORY_PRODUCTIVITY; applicationInfo.flags = FLAG_INSTALLED; return applicationInfo; }).when(mLauncherApps).getApplicationInfo(anyString(), anyInt(), any()); - AppWidgetManager manager = mModelHelper.sandboxContext.spyService(AppWidgetManager.class); + AppWidgetManager manager = mContext.spyService(AppWidgetManager.class); doReturn(allWidgets).when(manager).getInstalledProviders(); doReturn(allWidgets).when(manager).getInstalledProvidersForProfile(eq(myUserHandle())); doAnswer(i -> { @@ -134,17 +155,13 @@ public final class WidgetsPredicationUpdateTaskTest { LauncherLayoutBuilder builder = new LauncherLayoutBuilder() .atWorkspace(0, 1, 2).putWidget("app4", "provider1", 1, 1) .atWorkspace(0, 1, 3).putWidget("app5", "provider1", 1, 1); - mModelHelper.setupDefaultLayoutProvider(builder); - MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(mCallback)).get(); - mModelHelper.loadModelSync(); - } - - @After - public void tearDown() { - mModelHelper.destroy(); + mLayoutProvider.setupDefaultLayoutProvider(builder); + MAIN_EXECUTOR.submit(() -> getModel().addCallbacks(mCallback)).get(); + ModelTestExtensions.INSTANCE.loadModelSync(getModel()); } @Test + @DisableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER) // Flag off public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() { // Run on model executor so that no other task runs in the middle. runOnExecutorSync(MODEL_EXECUTOR, () -> { @@ -160,7 +177,7 @@ public final class WidgetsPredicationUpdateTaskTest { AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1", mUserHandle); mCallback.mRecommendedWidgets = null; - mModelHelper.getModel().enqueueModelUpdateTask( + getModel().enqueueModelUpdateTask( newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1))); runOnExecutorSync(MAIN_EXECUTOR, () -> { }); @@ -170,7 +187,8 @@ public final class WidgetsPredicationUpdateTaskTest { // 2. app3 doesn't have a widget. // 3. only 1 widget is picked from app1 because we only want to promote one widget // per app. - List recommendedWidgets = mCallback.mRecommendedWidgets.items + List recommendedWidgets = mCallback.mRecommendedWidgets + .getContents() .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); @@ -184,6 +202,7 @@ public final class WidgetsPredicationUpdateTaskTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER) // Flag off public void widgetsRecommendationRan_shouldReturnEmptyWidgetsWhenEmpty() { runOnExecutorSync(MODEL_EXECUTOR, () -> { @@ -200,12 +219,13 @@ public final class WidgetsPredicationUpdateTaskTest { mUserHandle); mCallback.mRecommendedWidgets = null; - mModelHelper.getModel().enqueueModelUpdateTask( + getModel().enqueueModelUpdateTask( newWidgetsPredicationTask(List.of(widget5, widget3, widget4, widget1))); runOnExecutorSync(MAIN_EXECUTOR, () -> { }); // Only widgets suggested by prediction system are returned. - List recommendedWidgets = mCallback.mRecommendedWidgets.items + List recommendedWidgets = mCallback.mRecommendedWidgets + .getContents() .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); @@ -213,6 +233,33 @@ public final class WidgetsPredicationUpdateTaskTest { }); } + @Test + public void widgetsRecommendations_excludesWidgetsHiddenForPicker() { + runOnExecutorSync(MODEL_EXECUTOR, () -> { + + // Not installed widget - hence eligible + AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", + mUserHandle); + // Provider marked as hidden from picker - hence not eligible + AppTarget widget6 = new AppTarget(new AppTargetId("app6"), "app6", "provider1", + mUserHandle); + + mCallback.mRecommendedWidgets = null; + getModel().enqueueModelUpdateTask( + newWidgetsPredicationTask(List.of(widget1, widget6))); + runOnExecutorSync(MAIN_EXECUTOR, () -> { }); + + // Only widget 1 (and no widget 6 as its meant to be hidden from picker). + List recommendedWidgets = mCallback.mRecommendedWidgets + .getContents() + .stream() + .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) + .collect(Collectors.toList()); + assertThat(recommendedWidgets).hasSize(1); + assertThat(recommendedWidgets.get(0).componentName.getPackageName()).isEqualTo("app1"); + }); + } + private void assertWidgetInfo( LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) { assertThat(actual.provider).isEqualTo(expected.provider); @@ -221,17 +268,27 @@ public final class WidgetsPredicationUpdateTaskTest { private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List appTargets) { return new WidgetsPredictionUpdateTask( - new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"), + new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction", + DEFAULT_LOOKUP_FLAG), appTargets); } + private LauncherModel getModel() { + return LauncherAppState.getInstance(mContext).getModel(); + } + private final class FakeBgDataModelCallback implements BgDataModel.Callbacks { - private FixedContainerItems mRecommendedWidgets = null; + private PredictedContainerInfo mRecommendedWidgets = null; @Override - public void bindExtraContainerItems(FixedContainerItems item) { - mRecommendedWidgets = item; + public void bindItemsUpdated(@NonNull Set updates) { + for (ItemInfo update : updates) { + if (update.id == CONTAINER_WIDGETS_PREDICTION + && update instanceof PredictedContainerInfo pci) { + mRecommendedWidgets = pci; + } + } } } } diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt new file mode 100644 index 0000000000..a91ee3a0f4 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.statehandlers + +import android.content.res.Resources +import android.platform.test.annotations.EnableFlags +import android.view.ViewTreeObserver +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Flags +import com.android.launcher3.Launcher +import com.android.launcher3.LauncherState +import com.android.launcher3.R +import com.android.launcher3.dragndrop.DragLayer +import com.android.launcher3.statemanager.StateManager +import com.android.launcher3.uioverrides.QuickstepLauncher +import java.util.Collections +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.same +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DepthControllerTest { + + private lateinit var underTest: DepthController + @Mock private lateinit var launcher: QuickstepLauncher + @Mock private lateinit var stateManager: StateManager + @Mock private lateinit var resource: Resources + @Mock private lateinit var dragLayer: DragLayer + @Mock private lateinit var viewTreeObserver: ViewTreeObserver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + `when`(launcher.resources).thenReturn(resource) + `when`(resource.getInteger(R.integer.max_depth_blur_radius)).thenReturn(30) + `when`(launcher.dragLayer).thenReturn(dragLayer) + `when`(dragLayer.viewTreeObserver).thenReturn(viewTreeObserver) + `when`(launcher.stateManager).thenReturn(stateManager) + `when`(launcher.depthBlurTargets).thenReturn(Collections.emptyList()) + + underTest = DepthController(launcher) + } + + @Test + fun setActivityStarted_add_onDrawListener() { + underTest.setActivityStarted(true) + + verify(viewTreeObserver).addOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun setActivityStopped_not_remove_onDrawListener() { + underTest.setActivityStarted(false) + + // Because underTest.mOnDrawListener is never added + verifyNoMoreInteractions(viewTreeObserver) + } + + @Test + fun setActivityStared_then_stopped_remove_onDrawListener() { + underTest.setActivityStarted(true) + reset(viewTreeObserver) + + underTest.setActivityStarted(false) + + verify(viewTreeObserver).removeOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun setActivityStared_then_stopped_multiple_times_remove_onDrawListener_once() { + underTest.setActivityStarted(true) + reset(viewTreeObserver) + + underTest.setActivityStarted(false) + underTest.setActivityStarted(false) + underTest.setActivityStarted(false) + + // Should just remove mOnDrawListener once + verify(viewTreeObserver).removeOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + fun test_onInvalidSurface_multiple_times_add_onDrawListener_once() { + underTest.onInvalidSurface() + underTest.onInvalidSurface() + underTest.onInvalidSurface() + + // We should only call addOnDrawListener 1 time + verify(viewTreeObserver).addOnDrawListener(same(underTest.mOnDrawListener)) + } + + @Test + @EnableFlags(Flags.FLAG_ALL_APPS_BLUR) + fun test_blurWorkspaceDepthTargets() { + // Transitioning to ALL_APPS from any state should blur the workspace depth targets. + + `when`(stateManager.currentStableState).thenReturn(LauncherState.NORMAL) + `when`(stateManager.state).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.state).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.SPRING_LOADED) + `when`(stateManager.state).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.EDIT_MODE) + `when`(stateManager.state).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.BACKGROUND_APP) + `when`(stateManager.state).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + // Returning from ALL_APPS to NORMAL should continue blurring the workspace depth targets. + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.state).thenReturn(LauncherState.NORMAL) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + // Exiting ALL_APPS to other states such as drag-and-drop should not blur the workspace. + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.state).thenReturn(LauncherState.SPRING_LOADED) + assertFalse(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.state).thenReturn(LauncherState.EDIT_MODE) + assertFalse(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.state).thenReturn(LauncherState.OVERVIEW) + assertFalse(underTest.blurWorkspaceDepthTargets()) + } + + @Test + @EnableFlags(Flags.FLAG_ALL_APPS_BLUR) + fun test_blurWorkspaceDepthTargets_withTargetState() { + // Transitioning to ALL_APPS from any state should blur the workspace depth targets. + + `when`(stateManager.currentStableState).thenReturn(LauncherState.NORMAL) + `when`(stateManager.targetState).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.targetState).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.SPRING_LOADED) + `when`(stateManager.targetState).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.EDIT_MODE) + `when`(stateManager.targetState).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.BACKGROUND_APP) + `when`(stateManager.targetState).thenReturn(LauncherState.ALL_APPS) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + // Returning from ALL_APPS to NORMAL should continue blurring the workspace depth targets. + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.targetState).thenReturn(LauncherState.NORMAL) + assertTrue(underTest.blurWorkspaceDepthTargets()) + + // Exiting ALL_APPS to other states such as drag-and-drop should not blur the workspace. + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.targetState).thenReturn(LauncherState.SPRING_LOADED) + assertFalse(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.targetState).thenReturn(LauncherState.EDIT_MODE) + assertFalse(underTest.blurWorkspaceDepthTargets()) + + `when`(stateManager.currentStableState).thenReturn(LauncherState.ALL_APPS) + `when`(stateManager.targetState).thenReturn(LauncherState.OVERVIEW) + assertFalse(underTest.blurWorkspaceDepthTargets()) + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java deleted file mode 100644 index 9ed39066dd..0000000000 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.taskbar; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - -import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.Display; -import android.view.MotionEvent; - -import androidx.test.filters.SmallTest; - -import com.android.launcher3.BubbleTextView; -import com.android.launcher3.folder.Folder; -import com.android.launcher3.folder.FolderIcon; -import com.android.launcher3.model.data.FolderInfo; -import com.android.launcher3.util.ActivityContextWrapper; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.stubbing.Answer; - -/** - * Tests for TaskbarHoverToolTipController. - */ -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase { - - private TaskbarHoverToolTipController mTaskbarHoverToolTipController; - private TestableLooper mTestableLooper; - - @Mock private TaskbarView mTaskbarView; - @Mock private MotionEvent mMotionEvent; - @Mock private BubbleTextView mHoverBubbleTextView; - @Mock private FolderIcon mHoverFolderIcon; - @Mock private Display mDisplay; - @Mock private TaskbarDragLayer mTaskbarDragLayer; - private Folder mSpyFolderView; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - Context context = getApplicationContext(); - - doAnswer((Answer) invocation -> context.getSystemService( - (String) invocation.getArgument(0))) - .when(taskbarActivityContext).getSystemService(anyString()); - when(taskbarActivityContext.getResources()).thenReturn(context.getResources()); - when(taskbarActivityContext.getApplicationInfo()).thenReturn( - context.getApplicationInfo()); - when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer); - when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper()); - when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay); - - when(mTaskbarDragLayer.getChildCount()).thenReturn(1); - mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null)); - when(mTaskbarDragLayer.getChildAt(anyInt())).thenReturn(mSpyFolderView); - doReturn(false).when(mSpyFolderView).isOpen(); - - when(mHoverBubbleTextView.getText()).thenReturn("tooltip"); - doAnswer((Answer) invocation -> { - Object[] args = invocation.getArguments(); - ((int[]) args[0])[0] = 0; - ((int[]) args[0])[1] = 0; - return null; - }).when(mHoverBubbleTextView).getLocationOnScreen(any(int[].class)); - when(mHoverBubbleTextView.getWidth()).thenReturn(100); - when(mHoverBubbleTextView.getHeight()).thenReturn(100); - - mHoverFolderIcon.mInfo = new FolderInfo(); - mHoverFolderIcon.mInfo.title = "tooltip"; - doAnswer((Answer) invocation -> { - Object[] args = invocation.getArguments(); - ((int[]) args[0])[0] = 0; - ((int[]) args[0])[1] = 0; - return null; - }).when(mHoverFolderIcon).getLocationOnScreen(any(int[].class)); - when(mHoverFolderIcon.getWidth()).thenReturn(100); - when(mHoverFolderIcon.getHeight()).thenReturn(100); - - when(mTaskbarView.getTop()).thenReturn(200); - - mTaskbarHoverToolTipController = new TaskbarHoverToolTipController( - taskbarActivityContext, mTaskbarView, mHoverBubbleTextView); - mTestableLooper = TestableLooper.get(this); - } - - @Test - public void onHover_hoverEnterIcon_revealToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - true); - } - - @Test - public void onHover_hoverExitIcon_closeToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - false); - } - - @Test - public void onHover_hoverEnterFolderIcon_revealToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - true); - } - - @Test - public void onHover_hoverExitFolderIcon_closeToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - false); - } - - @Test - public void onHover_hoverExitFolderOpen_closeToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT); - doReturn(true).when(mSpyFolderView).isOpen(); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - waitForIdleSync(); - - assertThat(hoverHandled).isTrue(); - verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, - false); - } - - @Test - public void onHover_hoverEnterFolderOpen_noToolTip() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); - doReturn(true).when(mSpyFolderView).isOpen(); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - - assertThat(hoverHandled).isFalse(); - } - - @Test - public void onHover_hoverMove_noUpdate() { - when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE); - when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE); - - boolean hoverHandled = - mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent); - - assertThat(hoverHandled).isFalse(); - } - - private void waitForIdleSync() { - mTestableLooper.processAllMessages(); - } -} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt deleted file mode 100644 index 104263af5b..0000000000 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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.launcher3.taskbar - -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.content.ComponentName -import android.content.Intent -import android.os.Process -import android.os.UserHandle -import android.testing.AndroidTestingRunner -import com.android.launcher3.model.data.AppInfo -import com.android.launcher3.model.data.ItemInfo -import com.android.launcher3.statehandlers.DesktopVisibilityController -import com.android.quickstep.RecentsModel -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnit -import org.mockito.kotlin.whenever - -@RunWith(AndroidTestingRunner::class) -class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() { - - @get:Rule val mockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var mockRecentsModel: RecentsModel - @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController - - private var nextTaskId: Int = 500 - - private lateinit var recentAppsController: TaskbarRecentAppsController - private lateinit var userHandle: UserHandle - - @Before - fun setUp() { - super.setup() - userHandle = Process.myUserHandle() - recentAppsController = - TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController } - recentAppsController.init(taskbarControllers) - recentAppsController.isEnabled = true - recentAppsController.setApps( - ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray() - ) - } - - @Test - fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() { - setInDesktopMode(false) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - - assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())) - .isEqualTo(hotseatItems.toTypedArray()) - } - - @Test - fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() { - setInDesktopMode(false) - val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2) - val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - assertThat(newHotseatItems.map { it?.targetPackage }) - .containsExactlyElementsIn(hotseatPackages) - } - - @Test - fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() { - setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - - assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())) - .isEqualTo(hotseatItems.toTypedArray()) - } - - @Test - fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() { - setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - val expectedPackages = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - ) - assertThat(newHotseatItems.map { it?.targetPackage }) - .containsExactlyElementsIn(expectedPackages) - } - - @Test - fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() { - setInDesktopMode(true) - val hotseatItems = - createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)) - val runningTasks = - createDesktopTasksFromPackageNames( - listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) - ) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - val newHotseatItems = - recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()) - - val expectedPackages = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - ) - assertThat(newHotseatItems.map { it?.targetPackage }) - .containsExactlyElementsIn(expectedPackages) - } - - @Test - fun getRunningApps_notInDesktopMode_returnsEmptySet() { - setInDesktopMode(false) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - assertThat(recentAppsController.runningApps).isEmpty() - assertThat(recentAppsController.minimizedApps).isEmpty() - } - - @Test - fun getRunningApps_inDesktopMode_returnsRunningApps() { - setInDesktopMode(true) - val runningTasks = - createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - assertThat(recentAppsController.runningApps) - .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2) - assertThat(recentAppsController.minimizedApps).isEmpty() - } - - @Test - fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() { - setInDesktopMode(true) - val runningTasks = - ArrayList( - listOf( - createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true }, - createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true }, - createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false }, - ) - ) - whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks) - recentAppsController.updateRunningApps() - - assertThat(recentAppsController.runningApps) - .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3) - assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3) - } - - private fun createHotseatItemsFromPackageNames(packageNames: List): List { - return packageNames.map { createTestAppInfo(packageName = it) } - } - - private fun createDesktopTasksFromPackageNames( - packageNames: List - ): ArrayList { - return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) }) - } - - private fun createDesktopTaskInfo( - packageName: String, - init: RunningTaskInfo.() -> Unit = { isVisible = true }, - ): RunningTaskInfo { - return RunningTaskInfo().apply { - taskId = nextTaskId++ - configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - realActivity = ComponentName(packageName, "TestActivity") - init() - } - } - - private fun createTestAppInfo( - packageName: String = "testPackageName", - className: String = "testClassName" - ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent()) - - private fun setInDesktopMode(inDesktopMode: Boolean) { - whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode) - } - - private companion object { - const val HOTSEAT_PACKAGE_1 = "hotseat1" - const val HOTSEAT_PACKAGE_2 = "hotseat2" - const val RUNNING_APP_PACKAGE_1 = "running1" - const val RUNNING_APP_PACKAGE_2 = "running2" - const val RUNNING_APP_PACKAGE_3 = "running3" - val ALL_APP_PACKAGES = - listOf( - HOTSEAT_PACKAGE_1, - HOTSEAT_PACKAGE_2, - RUNNING_APP_PACKAGE_1, - RUNNING_APP_PACKAGE_2, - RUNNING_APP_PACKAGE_3, - ) - } -} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt new file mode 100644 index 0000000000..0a9ee2c8b6 --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt @@ -0,0 +1,1655 @@ +/* + * 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.launcher3.taskbar.bubbles.animation + +import android.content.Context +import android.graphics.Color +import android.graphics.Path +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.animation.AnimatorTestRule +import androidx.core.graphics.drawable.toBitmap +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarInsetsController +import com.android.launcher3.taskbar.bubbles.BubbleBarBubble +import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow +import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController +import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage +import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner +import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks +import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleInfo +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarViewAnimatorTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler + private lateinit var bubbleBarParentViewController: TestBubbleBarParentViewHeightUpdateNotifier + private lateinit var overflowView: BubbleView + private lateinit var bubbleView: BubbleView + private lateinit var bubble: BubbleBarBubble + private lateinit var bubbleBarView: BubbleBarView + private lateinit var flyoutContainer: FrameLayout + private lateinit var bubbleStashController: FakeBubbleStashController + private lateinit var flyoutController: BubbleBarFlyoutController + private val emptyRunnable = Runnable {} + + private val flyoutView: View? + get() = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) + + @Before + fun setUp() { + animatorScheduler = TestBubbleBarViewAnimatorScheduler() + bubbleBarParentViewController = TestBubbleBarParentViewHeightUpdateNotifier() + bubbleStashController = FakeBubbleStashController() + PhysicsAnimatorTestUtils.prepareForTest() + setupFlyoutController() + } + + @Test + fun animateBubbleInForStashed() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // execute the hide bubble animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun animateBubbleInForStashed_tapAnimatingBubble() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + + assertThat(bubbleStashController.taskbarTouchRegionUpdated).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForTouch() } + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + } + + @Test + fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.onStashStateChangingWhileAnimating() + } + + // wait for the animation to cancel + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + handleAnimator, + DynamicAnimation.TRANSLATION_Y, + ) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.animationInterrupted).isTrue() + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleStashController.isStashed).isTrue() + + // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait + // again + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + handleAnimator.assertIsNotRunning() + } + + @Test + fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // execute the hide bubble animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // wait for the hide animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + handleAnimator.assertIsRunning() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.onStashStateChangingWhileAnimating() + } + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.animationInterrupted).isTrue() + + // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait + // again + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + handleAnimator.assertIsNotRunning() + } + + @Test + fun animateBubbleInForStashed_autoExpanding() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = true) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.isExpanded).isTrue() + + // verify there is no hide animation + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleInForStashed_expandedWhileAnimatingIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the animation finish + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleInForStashed_expandedWhileFullyIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // wait for the animation to end + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateToInitialState_inApp() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.IN_APP + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedBubbleBarVisible = false + val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = onBubbleBarVisible, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleBarView.visibility = INVISIBLE + animator.animateToInitialState(bubble, isInApp = true, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(handle.translationY).isEqualTo(0) + assertThat(handle.alpha).isEqualTo(1) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(notifiedBubbleBarVisible).isTrue() + + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun animateToInitialState_whileDragging_inApp() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.IN_APP + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedBubbleBarVisible = false + val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = onBubbleBarVisible, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleBarView.visibility = INVISIBLE + animator.animateToInitialState( + bubble, + isInApp = true, + isExpanding = false, + isDragging = true, + ) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(notifiedBubbleBarVisible).isTrue() + } + + @Test + fun animateToInitialState_inApp_autoExpanding() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.IN_APP + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateToInitialState_inHome() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + fun animateToInitialState_expandedWhileAnimatingIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true } + + bubbleBarAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the animation finish + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateToInitialState_expandedWhileFullyIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + waitForFlyoutToHide() + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + + verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(animator.isAnimating).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + + // the lift animation is complete; the spring back animation should start now + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + barAnimator.assertIsRunning() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + // the bubble bar translation y should be back to its initial value + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleStashController.isStashed).isFalse() + } + + @Test + fun animateBubbleBarForCollapsed_autoExpanding() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val semaphore = Semaphore(0) + var notifiedExpanded = false + val onExpanded = Runnable { + notifiedExpanded = true + semaphore.release() + } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = true) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + + // the lift animation is complete; the spring back animation should start now + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + // we should be expanded now + assertThat(bubbleBarView.isExpanded).isTrue() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify there is no hide animation + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val semaphore = Semaphore(0) + var notifiedExpanded = false + val onExpanded = Runnable { + notifiedExpanded = true + semaphore.release() + } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(100) + } + + // verify there is a pending hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + assertThat(animator.isAnimating).isTrue() + + // send the expand signal in the middle of the lift animation + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // let the lift animation complete + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(150) + } + + // the lift animation is complete; the spring back animation should start now. wait for it + // to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun animateBubbleBarForCollapsed_expandingWhileFullyIn() { + setUpBubbleBar() + bubbleStashController.launcherState = BubbleLauncherState.HOME + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedExpanded = false + val onExpanded = Runnable { notifiedExpanded = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = onExpanded, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + // verify we started animating + assertThat(animator.isAnimating).isTrue() + + // advance the animation handler by the duration of the initial lift + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + + // the lift animation is complete; the spring back animation should start now + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + barAnimator.assertIsRunning() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify there is a pending hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.expandedWhileAnimating() + } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + + waitForFlyoutToHide() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) + assertThat(bubbleBarView.isExpanded).isTrue() + assertThat(bubbleStashController.isStashed).isFalse() + assertThat(notifiedExpanded).isTrue() + } + + @Test + fun interruptAnimation_whileAnimatingIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait until the first frame + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + } + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptAnimation_whileIn() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + + // verify the hide animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + } + + // verify the hide animation was rescheduled + assertThat(animatorScheduler.canceledBlock).isNotNull() + assertThat(animatorScheduler.delayedBlock).isNotNull() + + waitForFlyoutToFadeOutAndBackIn() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptAnimation_whileAnimatingOut_whileCollapsingFlyout() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + // interrupt the animation while the flyout is collapsing + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(100) + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + + // the flyout should now reverse and expand + animatorTestRule.advanceTimeBy(400) + } + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + + // verify the hide animation was rescheduled and run it + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptAnimation_whileAnimatingOut_barToHandle() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("message") + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // interrupt the animation while the bar is animating to the handle + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { + bubbleBarView.alpha < 0.5 + } + // we're about to interrupt the animation which will cancel the current animation and start + // a new one. pause the scheduler to delay starting the new animation. this allows us to run + // the test deterministically + animatorScheduler.pauseScheduler = true + + val updatedBubble = + bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleView.setBubble(updatedBubble) + animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) + } + + // since animation was interrupted there shouldn't be additional calls to adjust window + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + // verify there's a new job scheduled and start it. this is starting the animation from the + // handle back to the bar + assertThat(animatorScheduler.pausedBlock).isNotNull() + animatorScheduler.pauseScheduler = false + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.pausedBlock!!) + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + waitForFlyoutToShow() + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) + .isEqualTo("updated message") + assertThat(handle.alpha).isEqualTo(0) + assertThat(handle.translationY) + .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + + // run the hide animation + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + // verify the hide animation was rescheduled and run it + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + // let the animation start and wait for it to complete + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3) + assertThat(handle.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.isStashed).isTrue() + } + + @Test + fun interruptForIme() { + setUpBubbleBar() + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + bubbleStashController.handleAnimator = handleAnimator + + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = emptyRunnable, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble, isExpanding = false) + } + + // wait for the animation to start + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } + + handleAnimator.assertIsRunning() + assertThat(animator.isAnimating).isTrue() + // verify the hide bubble animation is pending + assertThat(animatorScheduler.delayedBlock).isNotNull() + + InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForIme() } + + // verify that the hide animation was canceled + assertThat(animatorScheduler.delayedBlock).isNull() + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleStashController.animationInterrupted).isTrue() + assertThat(bubbleStashController.isStashed).isTrue() + + // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait + // again + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + handleAnimator.assertIsNotRunning() + } + + private fun setUpBubbleBar() { + bubbleBarView = BubbleBarView(context) + bubbleBarView.setController( + object : BubbleBarView.Controller { + override fun getBubbleBarTranslationY(): Float = 0f + + override fun onBubbleBarTouched() {} + + override fun expandBubbleBar() {} + + override fun dismissBubbleBar() {} + + override fun updateBubbleBarLocation(location: BubbleBarLocation?, source: Int) {} + + override fun setIsDragging(dragging: Boolean) {} + + override fun onBubbleBarExpandedStateChanged(expanded: Boolean) {} + } + ) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0) + val inflater = LayoutInflater.from(context) + + val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) + overflowView = + inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView + overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) + bubbleBarView.addView(overflowView) + + val bubbleInfo = + BubbleInfo( + "key", + 0, + null, + null, + 0, + context.packageName, + null, + null, + false, + true, + null, + ) + bubbleView = + inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView + bubble = + BubbleBarBubble( + bubbleInfo, + bubbleView, + bitmap, + bitmap, + Color.WHITE, + Path(), + "", + BubbleBarFlyoutMessage(icon = null, title = "title", message = "message"), + ) + bubbleView.setBubble(bubble) + bubbleBarView.addView(bubbleView) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + } + + private fun setupFlyoutController() { + flyoutContainer = FrameLayout(context) + val flyoutPositioner = + object : BubbleBarFlyoutPositioner { + override val isOnLeft = true + override val targetTy = 100f + override val distanceToCollapsedPosition = PointF(0f, 0f) + override val collapsedSize = 30f + override val collapsedColor = Color.BLUE + override val collapsedElevation = 1f + override val distanceToRevealTriangle = 10f + } + val flyoutCallbacks = + object : FlyoutCallbacks { + override fun flyoutClicked() {} + } + val flyoutScheduler = FlyoutScheduler { block -> block.invoke() } + flyoutController = + BubbleBarFlyoutController( + flyoutContainer, + flyoutPositioner, + flyoutCallbacks, + flyoutScheduler, + ) + } + + private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) { + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleBarView.scaleX).isEqualTo(1) + assertThat(bubbleBarView.scaleY).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(ty) + assertThat(bubbleBarView.isExpanded).isTrue() + } + + private fun waitForFlyoutToShow() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(400) + } + assertThat(flyoutView).isNotNull() + } + + private fun waitForFlyoutToHide() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(350) + } + assertThat(flyoutView).isNull() + } + + private fun waitForFlyoutToFadeOutAndBackIn() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(750) + } + assertThat(flyoutView).isNotNull() + } + + private fun PhysicsAnimator.assertIsRunning() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + assertThat(isRunning()).isTrue() + } + } + + private fun PhysicsAnimator.assertIsNotRunning() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + assertThat(isRunning()).isFalse() + } + } + + private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler { + + var pauseScheduler = false + var pausedBlock: Runnable? = null + private set + + var delayedBlock: Runnable? = null + private set + + var canceledBlock: Runnable? = null + private set + + override fun post(block: Runnable) { + if (pauseScheduler) { + pausedBlock = block + return + } + block.run() + } + + override fun postDelayed(delayMillis: Long, block: Runnable) { + delayedBlock = block + } + + override fun cancel(block: Runnable) { + canceledBlock = delayedBlock + delayedBlock = null + } + } + + private class TestBubbleBarParentViewHeightUpdateNotifier : + BubbleBarParentViewHeightUpdateNotifier { + + var timesInvoked: Int = 0 + + override fun updateTopBoundary() { + timesInvoked++ + } + } + + private class FakeBubbleStashController : BubbleStashController { + + var handleAnimator: PhysicsAnimator? = null + var taskbarTouchRegionUpdated = false + private set + + var animationInterrupted = false + private set + + private var _isStashed = true + + override var launcherState = BubbleLauncherState.HOME + override val isStashed: Boolean + get() = _isStashed + + override var bubbleBarVerticalCenterForHome = 0 + override var isSysuiLocked = false + override val isTransientTaskBar = true + override val hasHandleView = true + override val bubbleBarTranslationYForTaskbar = BAR_TRANSLATION_Y_FOR_TASKBAR + override val bubbleBarTranslationYForHotseat = BAR_TRANSLATION_Y_FOR_HOTSEAT + override var inAppDisplayOverrideProgress = 0f + + override fun init( + taskbarInsetsController: TaskbarInsetsController, + bubbleBarViewController: BubbleBarViewController, + bubbleStashedHandleViewController: BubbleStashedHandleViewController?, + controllersAfterInitAction: BubbleStashController.ControllersAfterInitAction, + ) {} + + override fun showBubbleBarImmediate() { + _isStashed = false + } + + override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) { + _isStashed = false + } + + override fun stashBubbleBarImmediate() { + _isStashed = true + } + + override fun getTouchableHeight() = 100 + + override fun isBubbleBarVisible() = true + + override fun onNewBubbleAnimationInterrupted( + isStashed: Boolean, + bubbleBarTranslationY: Float, + ) { + _isStashed = isStashed + animationInterrupted = true + } + + override fun isEventOverBubbleBarViews(ev: MotionEvent) = false + + override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {} + + override fun stashBubbleBar() { + _isStashed = true + } + + override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) { + _isStashed = false + } + + override fun getDiffBetweenHandleAndBarCenters() = DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + + override fun getStashedHandleTranslationForNewBubbleAnimation() = HANDLE_TRANSLATION + + override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? { + return handleAnimator + } + + override fun updateTaskbarTouchRegion() { + taskbarTouchRegionUpdated = true + } + + override fun setHandleTranslationY(translationY: Float) {} + + override fun getHandleTranslationY() = 0f + + override fun getHandleBounds(bounds: Rect) {} + } +} + +private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f +private const val HANDLE_TRANSLATION = -30f +private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f +private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java index 44c23ba9a2..2a066c20cd 100644 --- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java +++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java @@ -16,25 +16,40 @@ package com.android.quickstep; +import static com.android.quickstep.fallback.RecentsStateUtilsKt.hasEquivalentRecentsState; +import static com.android.quickstep.fallback.RecentsStateUtilsKt.toLauncherState; + import static org.junit.Assert.assertTrue; import android.os.SystemProperties; +import androidx.annotation.Nullable; import androidx.test.uiautomator.By; import androidx.test.uiautomator.Until; +import com.android.launcher3.LauncherState; import com.android.launcher3.tapl.LaunchedAppState; -import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.TestUtil; +import com.android.launcher3.util.Wait; +import com.android.launcher3.util.ui.AbstractLauncherUiTest; +import com.android.quickstep.fallback.window.RecentsWindowFlags; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.views.RecentsView; import org.junit.rules.RuleChain; import org.junit.rules.TestRule; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + /** * Base class for all instrumentation tests that deal with Quickstep. */ -public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest { +public abstract class AbstractQuickStepTest + extends AbstractLauncherUiTest> { public static final boolean ENABLE_SHELL_TRANSITIONS = SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @Override @@ -47,16 +62,100 @@ public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest condition) { + waitForRecentsWindowCondition(message, condition, TestUtil.DEFAULT_UI_TIMEOUT); + } + + // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide + // flakiness. + protected void waitForRecentsWindowCondition( + String message, Function condition, long timeout) { + verifyKeyguardInvisible(); + if (!TestHelpers.isInLauncherProcess()) return; + Wait.atMost(message, () -> getFromRecentsWindow(condition), mLauncher, timeout); + } + + protected T getFromRecentsWindowIfPresent(Function f) { + if (!TestHelpers.isInLauncherProcess()) return null; + return getFromRecentsWindow(recentsWindowManager -> + recentsWindowManager == null ? null : f.apply(recentsWindowManager)); + } + + protected T getFromRecentsWindow(Function f) { + if (!TestHelpers.isInLauncherProcess()) return null; + return getOnUiThread(() -> + f.apply(RecentsWindowManager.getRecentsWindowTracker().getCreatedContext())); + } + + protected void executeOnRecentsWindowIfPresent(Consumer f) { + if (!TestHelpers.isInLauncherProcess()) return; + getFromRecentsWindowIfPresent(recentsWindowManager -> { + f.accept(recentsWindowManager); + return null; + }); + } + + @Override + protected boolean isInState(Supplier state) { + if (!TestHelpers.isInLauncherProcess()) return true; + if (!RecentsWindowFlags.enableLauncherOverviewInWindow.isTrue() + || !hasEquivalentRecentsState(state.get())) { + return super.isInState(state); + } + return getFromRecentsWindow(recentsWindowManager -> + recentsWindowManager != null && toLauncherState( + recentsWindowManager.getStateManager().getState()) == state.get()); + } + + @Override + protected void waitForState( + boolean forInitialization, String message, Supplier state) { + if (!TestHelpers.isInLauncherProcess()) return; + if (!RecentsWindowFlags.enableLauncherOverviewInWindow.isTrue() + || !hasEquivalentRecentsState(state.get()) + || (forInitialization + && RecentsWindowManager.getRecentsWindowTracker().getCreatedContext() == null)) { + super.waitForState(forInitialization, message, state); + return; + } + waitForRecentsWindowCondition(message, recentsWindowManager -> + recentsWindowManager != null && toLauncherState( + recentsWindowManager.getStateManager().getState()) == state.get()); + } + + @Override + @Nullable + protected RecentsView getOverviewPanel() { + if (!TestHelpers.isInLauncherProcess()) return null; + if (!RecentsWindowFlags.enableLauncherOverviewInWindow.isTrue()) { + return super.getOverviewPanel(); + } + return getFromRecentsWindowIfPresent(RecentsWindowManager::getOverviewPanel); + } + + @Override + protected boolean useNullOverview() { + return super.useNullOverview() + && !RecentsWindowFlags.enableLauncherOverviewInWindow.isTrue(); + } + protected void assertTestActivityIsRunning(int activityNumber, String message) { assertTrue(message, mDevice.wait( Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity" + activityNumber)), - DEFAULT_UI_TIMEOUT)); + TestUtil.DEFAULT_UI_TIMEOUT)); } protected LaunchedAppState getAndAssertLaunchedApp() { diff --git a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java index fc757b44c9..a3a12bfba0 100644 --- a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java @@ -24,10 +24,10 @@ import android.content.Intent; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.Taskbar; -import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.TestUtil; +import com.android.launcher3.util.ui.AbstractLauncherUiTest; import org.junit.After; import org.junit.Assume; @@ -55,7 +55,9 @@ public class AbstractTaplTestsTaskbar extends AbstractQuickStepTest { "com.android.launcher3.testcomponent.BaseTestingActivity"); mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, layoutBuilder); AbstractLauncherUiTest.initialize(this); - startAppFast(CALCULATOR_APP_PACKAGE); + if (startCalendarAppDuringSetup()) { + startAppFast(CALCULATOR_APP_PACKAGE); + } mLauncher.enableBlockTimeout(true); mLauncher.showTaskbarIfHidden(); } @@ -72,8 +74,20 @@ public class AbstractTaplTestsTaskbar extends AbstractQuickStepTest { return DisplayController.isTransientTaskbar(context); } + protected boolean startCalendarAppDuringSetup() { + return true; + } + + protected boolean expectTaskbarIconsMatchHotseat() { + return true; + } + protected Taskbar getTaskbar() { Taskbar taskbar = mLauncher.getLaunchedAppState().getTaskbar(); + if (!expectTaskbarIconsMatchHotseat()) { + return taskbar; + } + List taskbarIconNames = taskbar.getIconNames(); List hotseatIconNames = mLauncher.getHotseatIconNames(); diff --git a/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt new file mode 100644 index 0000000000..fa245da495 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.content.ComponentName +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings +import android.view.Display.DEFAULT_DISPLAY +import android.view.LayoutInflater +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskContentView +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.logging.StatsLogManager.StatsLogger +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.TaskViewItemInfo +import com.android.launcher3.util.RunnableList +import com.android.launcher3.util.SandboxContext +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.launcher3.util.WindowBounds +import com.android.quickstep.TaskViewTestDIHelpers.initializeRecentsDependencies +import com.android.quickstep.TaskViewTestDIHelpers.mockRecentsModel +import com.android.quickstep.orientation.LandscapePagedViewHandler +import com.android.quickstep.recents.di.RecentsDependencies +import com.android.quickstep.task.thumbnail.TaskContentView +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.util.RecentsOrientedState +import com.android.quickstep.util.SingleTask +import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.eq +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Test for [AspectRatioSystemShortcut] */ +class AspectRatioSystemShortcutTests { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + /** Spy on a concrete Context so we can reference real View, Layout, and Display properties. */ + private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext) + private val applicationContext = SandboxContext(context) + + /** + * RecentsViewContainer and its super-interface ActivityContext contain methods to convert + * themselves to a Context at runtime, and static methods to convert a Context back to + * themselves by traversing ContextWrapper layers. + * + * Thus there is an undocumented assumption that a RecentsViewContainer always extends Context. + * We need to mock all of the RecentsViewContainer methods but leave the Context-under-test + * intact. + * + * The simplest way is to extend ContextWrapper and delegate the RecentsViewContainer interface + * to a mock. + */ + class RecentsViewContainerContextWrapper(base: Context) : + ContextWrapper(base), RecentsViewContainer by mock() { + + private val statsLogManager: StatsLogManager = mock() + + override fun getStatsLogManager(): StatsLogManager = statsLogManager + + override fun startActivitySafely(v: View, intent: Intent, item: ItemInfo?): RunnableList? = + null + } + + /** + * This is implicitly required in many parts of Launcher that + * require a Context. See RecentsViewContainerContextWrapper. + */ + private val launcher: RecentsViewContainerContextWrapper = + spy(RecentsViewContainerContextWrapper(context)) + + private val recentsView: LauncherRecentsView = mock() + private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val taskOverlayFactory: TaskOverlayFactory = + mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + private val factory: TaskShortcutFactory = + AspectRatioSystemShortcut.createFactory(abstractFloatingViewHelper) + private val statsLogger = mock() + private val orientedState: RecentsOrientedState = + mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + private lateinit var taskView: TaskView + + @Before + fun setUp() { + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + + val statsLogManager = launcher.getStatsLogManager() + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + + whenever(orientedState.orientationHandler).thenReturn(LandscapePagedViewHandler()) + + if (enableRefactorTaskThumbnail()) { + applicationContext.initDaggerComponent( + DaggerTaskViewTestComponent.builder().bindRecentsModel(mockRecentsModel()) + ) + initializeRecentsDependencies(launcher) + } + taskView = + LayoutInflater.from(context).cloneInContext(launcher).inflate(R.layout.task, null) + as TaskView + taskView.setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) + } + + @After + fun tearDown() { + if (enableRefactorTaskThumbnail()) { + RecentsDependencies.destroy(launcher) + } + } + + /** + * When the corresponding feature flag is off, there will not be an option to open aspect ratio + * settings. + */ + @DisableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT) + @Test + fun createShortcut_flaggedOff_notCreated() { + val task = createTask() + val taskContainer = createTaskContainer(task) + + setScreenSizeDp(widthDp = 1200, heightDp = 800) + taskView.bind(SingleTask(task), orientedState, taskOverlayFactory) + + assertThat(factory.getShortcuts(launcher, taskContainer)).isNull() + } + + /** + * When the screen doesn't meet or exceed sw600dp (eg. phone, watch), there will not be an + * option to open aspect ratio settings. + */ + @EnableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT) + @Test + fun createShortcut_sw599dp_notCreated() { + val task = createTask() + val taskContainer = createTaskContainer(task) + + setScreenSizeDp(widthDp = 599, heightDp = 599) + taskView.bind(SingleTask(task), orientedState, taskOverlayFactory) + + assertThat(factory.getShortcuts(launcher, taskContainer)).isNull() + } + + /** + * When the screen does meet or exceed sw600dp (eg. tablet, inner foldable screen, home cinema) + * there will be an option to open aspect ratio settings. + */ + @EnableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT) + @Test + fun createShortcut_sw800dp_created_andOpensSettings() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + val taskViewItemInfo = mock() + doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo + + setScreenSizeDp(widthDp = 1200, heightDp = 800) + taskView.bind(SingleTask(task), orientedState, taskOverlayFactory) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).hasSize(1) + + // On clicking the shortcut: + val shortcut = shortcuts!!.first() as AspectRatioSystemShortcut + shortcut.onClick(taskView) + + // 1) Panel should be closed + val allTypesExceptRebindSafe = + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe) + + // 2) Compat mode settings activity should be launched + val intentCaptor = argumentCaptor() + verify(launcher) + .startActivitySafely(any(), intentCaptor.capture(), eq(taskViewItemInfo)) + val intent = intentCaptor.firstValue!! + assertThat(intent.action).isEqualTo(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS) + + // 3) Shortcut tap event should be reported + verify(statsLogger).withItemInfo(taskViewItemInfo) + verify(statsLogger).log(LauncherEvent.LAUNCHER_ASPECT_RATIO_SETTINGS_SYSTEM_SHORTCUT_TAP) + } + + /** + * Overrides the screen size reported in the DeviceProfile, keeping the same pixel density as + * the underlying device and adjusting the pixel width/height to match what is required. + */ + private fun setScreenSizeDp(widthDp: Int, heightDp: Int) { + val density = context.resources.configuration.densityDpi + val widthPx = widthDp * density / 160 + val heightPx = heightDp * density / 160 + + val screenBounds = WindowBounds(widthPx, heightPx, widthPx, heightPx, Surface.ROTATION_0) + val deviceProfile = + InvariantDeviceProfile.INSTANCE[context].getDeviceProfile(context) + .toBuilder(context) + .setWindowBounds(screenBounds) + .build() + whenever(launcher.getDeviceProfile()).thenReturn(deviceProfile) + } + + /** Create a (very) fake task for testing. */ + private fun createTask() = + Task( + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + ComponentName("", ""), + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + ComponentName("", ""), + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + ) + + /** Create TaskContainer out of a given Task and fill in the rest with mocks. */ + private fun createTaskContainer(task: Task) = + TaskContainer( + taskView, + task, + when { + enableRefactorTaskContentView() -> mock() + enableRefactorTaskThumbnail() -> mock() + else -> mock() + }, + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + taskOverlayFactory, + ) +} diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt index 50b5df13f2..de1df0749d 100644 --- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt +++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt @@ -17,34 +17,50 @@ package com.android.quickstep import android.content.ComponentName +import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule -import com.android.dx.mockito.inline.extended.ExtendedMockito +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.platform.app.InstrumentationRegistry import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.R import com.android.launcher3.AbstractFloatingView import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags +import com.android.launcher3.Flags.enableRefactorTaskContentView +import com.android.launcher3.Flags.enableRefactorTaskThumbnail import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.logging.StatsLogManager.LauncherEvent -import com.android.launcher3.model.data.WorkspaceItemInfo -import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.model.data.TaskViewItemInfo import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.TransformingTouchDelegate import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.task.thumbnail.TaskContentView +import com.android.quickstep.task.thumbnail.TaskThumbnailView import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer import com.android.quickstep.views.TaskThumbnailViewDeprecated import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskViewIcon +import com.android.quickstep.views.TaskViewType import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.Task.TaskKey -import com.android.window.flags.Flags -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.google.common.truth.Truth.assertThat +import kotlin.test.assertIs +import kotlin.test.assertNotNull import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.Mockito.`when` import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq @@ -54,25 +70,22 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness -/** Test for DesktopSystemShortcut */ +/** Test for [DesktopSystemShortcut] */ +// TODO(b/403558856): Improve test coverage for DesktopModeCompatPolicy integration. class DesktopSystemShortcutTest { @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) - private val launcher: QuickstepLauncher = mock() + private val launcher: RecentsViewContainer = mock() private val statsLogManager: StatsLogManager = mock() private val statsLogger: StatsLogManager.StatsLogger = mock() private val recentsView: LauncherRecentsView = mock() - private val taskView: TaskView = mock() - private val workspaceItemInfo: WorkspaceItemInfo = mock() private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() - private val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = mock() - private val iconView: TaskViewIcon = mock() - private val transformingTouchDelegate: TransformingTouchDelegate = mock() + private val overlayFactory: TaskOverlayFactory = mock() private val factory: TaskShortcutFactory = DesktopSystemShortcut.createFactory(abstractFloatingViewHelper) - private val overlayFactory: TaskOverlayFactory = mock() - private val overlay: TaskOverlay<*> = mock() + private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext) + private val taskView: TaskView = createTaskViewMock() private lateinit var mockitoSession: StaticMockitoSession @@ -81,11 +94,12 @@ class DesktopSystemShortcutTest { mockitoSession = mockitoSession() .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) + .mockStatic(DesktopModeStatus::class.java) .startMocking() - ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() } - ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - whenever(overlayFactory.createOverlay(any())).thenReturn(overlay) + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + whenever(overlayFactory.createOverlay(any())).thenReturn(mock>()) + doReturn(DEFAULT_DISPLAY).whenever(context).displayId + whenever(launcher.asContext()).thenReturn(context) } @After @@ -95,22 +109,7 @@ class DesktopSystemShortcutTest { @Test fun createDesktopTaskShortcutFactory_desktopModeDisabled() { - setFlagsRule.disableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - - val task = - Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { - isDockable = true - } - val taskContainer = createTaskContainer(task) - - val shortcuts = factory.getShortcuts(launcher, taskContainer) - assertThat(shortcuts).isNull() - } - - @Test - fun createDesktopTaskShortcutFactory_desktopModeEnabled_DeviceNotSupported() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + `when`(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) val taskContainer = createTaskContainer(createTask()) @@ -119,22 +118,102 @@ class DesktopSystemShortcutTest { } @Test - fun createDesktopTaskShortcutFactory_desktopModeEnabled_DeviceNotSupported_OverrideEnabled() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() } - - val taskContainer = spy(createTaskContainer(createTask())) - doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo - + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun createDesktopTaskShortcutFactory_noDisplayActivity() { + val baseComponent = ComponentName("", /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ true, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey)) val shortcuts = factory.getShortcuts(launcher, taskContainer) - assertThat(shortcuts).isNotNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun createDesktopTaskShortcutFactory_transparentTask() { + val baseComponent = ComponentName("", /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ true, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun createDesktopTaskShortcutFactory_systemUiTask() { + val sysUiPackageName: String = context.resources.getString(R.string.config_systemUi) + val baseComponent = ComponentName(sysUiPackageName, /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun createDesktopTaskShortcutFactory_defaultHomeTask() { + val packageManager: PackageManager = mock() + whenever(context.packageManager).thenReturn(packageManager) + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + homeActivities, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + homeActivities, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey).apply { isDockable = true }) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() } @Test fun createDesktopTaskShortcutFactory_undockable() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - val unDockableTask = createTask().apply { isDockable = false } val taskContainer = createTaskContainer(unDockableTask) @@ -143,27 +222,47 @@ class DesktopSystemShortcutTest { } @Test - fun desktopSystemShortcutClicked() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - + fun desktopSystemShortcutClickedWithoutDesktopModeOnDisplay() { val task = createTask() val taskContainer = spy(createTaskContainer(task)) + whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(any(), any())).thenReturn(false) whenever(launcher.getOverviewPanel()).thenReturn(recentsView) whenever(launcher.statsLogManager).thenReturn(statsLogManager) whenever(statsLogManager.logger()).thenReturn(statsLogger) whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(taskView.context).thenReturn(context) whenever(recentsView.moveTaskToDesktop(any(), any(), any())).thenAnswer { val successCallback = it.getArgument(2) successCallback.run() } - doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + val taskViewItemInfo = mock() + doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo val shortcuts = factory.getShortcuts(launcher, taskContainer) - assertThat(shortcuts).hasSize(1) - assertThat(shortcuts!!.first()).isInstanceOf(DesktopSystemShortcut::class.java) + assertThat(shortcuts).isNull() + } - val desktopShortcut = shortcuts.first() as DesktopSystemShortcut + @Test + fun desktopSystemShortcutClickedWithDesktopModeOnDisplay() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + + whenever(DesktopModeStatus.isDesktopModeSupportedOnDisplay(any(), any())).thenReturn(true) + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + whenever(launcher.statsLogManager).thenReturn(statsLogManager) + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(taskView.context).thenReturn(context) + whenever(recentsView.moveTaskToDesktop(any(), any(), any())).thenAnswer { + val successCallback = it.getArgument(2) + successCallback.run() + } + val taskViewItemInfo = mock() + doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = assertNotNull(factory.getShortcuts(launcher, taskContainer)) + val desktopShortcut = assertIs(shortcuts.single()) desktopShortcut.onClick(taskView) @@ -174,29 +273,70 @@ class DesktopSystemShortcutTest { .moveTaskToDesktop( eq(taskContainer), eq(DesktopModeTransitionSource.APP_FROM_OVERVIEW), - any() + any(), ) - verify(statsLogger).withItemInfo(workspaceItemInfo) + verify(statsLogger).withItemInfo(taskViewItemInfo) verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP) } - private fun createTask(): Task { - return Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000)).apply { - isDockable = true - } + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MENU_ON_SECONDARY_DISPLAY_BUGFIX) + fun createDesktopTaskShortcutFactoryOnSecondaryDisplayWithoutFlag() { + `when`(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + `when`(DesktopModeStatus.isDesktopModeSupportedOnDisplay(any(), any())).thenReturn(true) + doReturn(SECONDARY_DISPLAY).whenever(context).displayId + + val taskContainer = createTaskContainer(createTask(displayId = SECONDARY_DISPLAY)) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() } - private fun createTaskContainer(task: Task): TaskView.TaskContainer { - return taskView.TaskContainer( + private fun createTask(displayId: Int = DEFAULT_DISPLAY) = + Task( + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + ComponentName("", ""), + /* userId */ 0, + /* lastActiveTime */ 2000, + displayId, + ComponentName("", ""), + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + ) + .apply { isDockable = true } + + private fun createTaskContainer(task: Task) = + TaskContainer( + taskView, task, - thumbnailView = null, - thumbnailViewDeprecated, - iconView, - transformingTouchDelegate, + when { + enableRefactorTaskContentView() -> mock() + enableRefactorTaskThumbnail() -> mock() + else -> mock() + }, + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, digitalWellBeingToast = null, showWindowsView = null, - overlayFactory + overlayFactory, ) + + private fun createTaskViewMock(): TaskView { + val taskView: TaskView = mock() + whenever(taskView.type).thenReturn(TaskViewType.SINGLE) + whenever(taskView.context).thenReturn(context) + return taskView + } + + private companion object { + const val SECONDARY_DISPLAY = 13 } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java similarity index 54% rename from quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java rename to quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java index 07d8f61992..fb3d7d6b19 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java +++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java @@ -15,24 +15,33 @@ */ package com.android.quickstep; -import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.launcher3.util.TestUtil.resolveSystemAppInfo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.app.PendingIntent; import android.app.usage.UsageStatsManager; import android.content.Intent; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.BaseLauncherActivityTest; +import com.android.launcher3.util.rule.ScreenRecordRule; import com.android.quickstep.views.DigitalWellBeingToast; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,41 +49,50 @@ import java.time.Duration; @LargeTest @RunWith(AndroidJUnit4.class) -public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { - private static final String CALCULATOR_PACKAGE = - resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR); +public class DigitalWellBeingToastTest extends BaseLauncherActivityTest { + + @Rule + public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule(); + + + public final String calculatorPackage = + resolveSystemAppInfo(Intent.CATEGORY_APP_CALCULATOR).packageName; @Test - public void testToast() throws Exception { - startAppFast(CALCULATOR_PACKAGE); + public void testToast() { + startAppFast(calculatorPackage); final UsageStatsManager usageStatsManager = - mTargetContext.getSystemService(UsageStatsManager.class); + targetContext().getSystemService(UsageStatsManager.class); final int observerId = 0; try { - final String[] packages = new String[]{CALCULATOR_PACKAGE}; + final String[] packages = new String[]{calculatorPackage}; // Set time limit for app. runWithShellPermission(() -> usageStatsManager.registerAppUsageLimitObserver(observerId, packages, Duration.ofSeconds(600), Duration.ofSeconds(300), - PendingIntent.getActivity(mTargetContext, -1, new Intent() - .setPackage(mTargetContext.getPackageName()), + PendingIntent.getActivity(targetContext(), -1, new Intent() + .setPackage(targetContext().getPackageName()), PendingIntent.FLAG_MUTABLE))); - mLauncher.goHome(); + // b/324261526 when removing BaseLauncherActivityTest we should be able to tell test + // by test test if they should start the activity from the start or wait, and that + // should get rid of this. + getLauncherActivity().close(); + loadLauncherSync(); final DigitalWellBeingToast toast = getToast(); - waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit()); - assertEquals("Toast text: ", "5 minutes left today", toast.getText()); + waitForLauncherCondition("Toast is not visible", launcher -> toast.getHasLimit()); + assertEquals("Toast text: ", "5 minutes left today", toast.getBannerText()); // Unset time limit for app. runWithShellPermission( () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId)); - mLauncher.goHome(); - assertFalse("Toast is visible", getToast().hasLimit()); + getLauncherActivity().goToState(LauncherState.NORMAL); + assertFalse("Toast is visible", getToast().getHasLimit()); } finally { runWithShellPermission( () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId)); @@ -82,19 +100,23 @@ public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { } private DigitalWellBeingToast getToast() { - mLauncher.getWorkspace().switchToOverview(); - final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher)); + getLauncherActivity().goToState(LauncherState.OVERVIEW); + final TaskView task = getLauncherActivity().getOnceNotNull( + "No latest task", + launcher -> getLatestTask(launcher) + ); - return getFromLauncher(launcher -> { - TaskView.TaskContainer taskContainer = task.getTaskContainers().get(0); - assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals( + return getLauncherActivity().getFromLauncher(launcher -> { + TaskContainer taskContainer = task.getFirstTaskContainer(); + assertNotNull(taskContainer); + assertTrue("Latest task is not Calculator", calculatorPackage.equals( taskContainer.getTask().getTopComponent().getPackageName())); return taskContainer.getDigitalWellBeingToast(); }); } private TaskView getLatestTask(Launcher launcher) { - return launcher.getOverviewPanel().getTaskViewAt(0); + return launcher.getOverviewPanel().getFirstTaskView(); } private void runWithShellPermission(Runnable action) { @@ -104,6 +126,5 @@ public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest { } finally { getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); } - } } diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt new file mode 100644 index 0000000000..9b384318df --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt @@ -0,0 +1,289 @@ +/* + * 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.quickstep + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.R +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskContentView +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.model.data.TaskViewItemInfo +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.task.thumbnail.TaskContentView +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +/** Test for [ExternalDisplaySystemShortcut] */ +class ExternalDisplaySystemShortcutTest { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + private val launcher: RecentsViewContainer = mock() + private val statsLogManager: StatsLogManager = mock() + private val statsLogger: StatsLogManager.StatsLogger = mock() + private val recentsView: LauncherRecentsView = mock() + private val taskView: TaskView = mock() + private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val overlayFactory: TaskOverlayFactory = mock() + private val factory: TaskShortcutFactory = + ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper) + private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext) + + private lateinit var mockitoSession: StaticMockitoSession + + @Before + fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus::class.java) + .startMocking() + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + whenever(overlayFactory.createOverlay(any())).thenReturn(mock>()) + whenever(launcher.asContext()).thenReturn(context) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT) + fun createExternalDisplayTaskShortcut_desktopModeDisabled() { + `when`(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) + + val taskContainer = createTaskContainer(createTask()) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + ) + fun createExternalDisplayTaskShortcut_noDisplayActivity() { + val baseComponent = ComponentName("", /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ true, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + ) + fun createExternalDisplayTaskShortcut_transparentTask() { + val baseComponent = ComponentName("", /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ true, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + ) + fun createExternalDisplayTaskShortcut_systemUiTask() { + val sysUiPackageName: String = context.resources.getString(R.string.config_systemUi) + val baseComponent = ComponentName(sysUiPackageName, /* class */ "") + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + baseComponent, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + baseComponent, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey)) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + ) + fun createExternalDisplayTaskShortcut_defaultHomeTask() { + val packageManager: PackageManager = mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + whenever(context.packageManager).thenReturn(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + val taskKey = + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + homeActivities, + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + homeActivities, + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + val taskContainer = createTaskContainer(Task(taskKey).apply { isDockable = true }) + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT) + fun externalDisplaySystemShortcutClicked() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + whenever(launcher.statsLogManager).thenReturn(statsLogManager) + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer { + val successCallback = it.getArgument(1) + successCallback.run() + } + val taskViewItemInfo = mock() + doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).hasSize(1) + assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java) + + val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut + + externalDisplayShortcut.onClick(taskView) + + val allTypesExceptRebindSafe = + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe) + verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any()) + verify(statsLogger).withItemInfo(taskViewItemInfo) + verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + + private fun createTask() = + Task( + TaskKey( + /* id */ 1, + /* windowingMode */ 0, + Intent(), + ComponentName("", ""), + /* userId */ 0, + /* lastActiveTime */ 2000, + DEFAULT_DISPLAY, + ComponentName("", ""), + /* numActivities */ 1, + /* isTopActivityNoDisplay */ false, + /* isActivityStackTransparent */ false, + ) + ) + + private fun createTaskContainer(task: Task) = + TaskContainer( + taskView, + task, + when { + enableRefactorTaskContentView() -> mock() + enableRefactorTaskThumbnail() -> mock() + else -> mock() + }, + if (enableRefactorTaskThumbnail()) mock() + else mock(), + mock(), + mock(), + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + overlayFactory, + ) +} diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java index 28589291fd..2485208e83 100644 --- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java +++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java @@ -16,22 +16,21 @@ package com.android.quickstep; import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; +import static android.view.Display.DEFAULT_DISPLAY; import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage; import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess; -import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_ACTIVITY_TIMEOUT; -import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_BROADCAST_TIMEOUT_SECS; -import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT; -import static com.android.launcher3.ui.AbstractLauncherUiTest.resolveSystemApp; -import static com.android.launcher3.ui.AbstractLauncherUiTest.startAppFast; -import static com.android.launcher3.ui.AbstractLauncherUiTest.startTestActivity; import static com.android.launcher3.ui.TaplTestsLauncher3Test.getAppPackageName; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.rule.ShellCommandRule.disableHeadsUpNotification; import static com.android.launcher3.util.rule.ShellCommandRule.getLauncherCommand; +import static com.android.launcher3.util.ui.AbstractLauncherUiTest.DEFAULT_BROADCAST_TIMEOUT_SECS; +import static com.android.launcher3.util.ui.AbstractLauncherUiTest.resolveSystemApp; +import static com.android.launcher3.util.ui.AbstractLauncherUiTest.startAppFast; +import static com.android.launcher3.util.ui.AbstractLauncherUiTest.startTestActivity; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -42,6 +41,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.RemoteException; +import android.platform.test.rule.ExtendedLongPressTimeoutRule; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; @@ -55,16 +55,20 @@ import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.OverviewTask; import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.testcomponent.TestCommandReceiver; -import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.util.TestUtil; import com.android.launcher3.util.Wait; -import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule; import com.android.launcher3.util.rule.FailureWatcher; import com.android.launcher3.util.rule.SamplerRule; import com.android.launcher3.util.rule.ScreenRecordRule; +import com.android.launcher3.util.rule.SkipAfterTimeOutRule; import com.android.launcher3.util.rule.TestIsolationRule; import com.android.launcher3.util.rule.TestStabilityRule; -import com.android.launcher3.util.rule.ViewCaptureRule; +import com.android.launcher3.util.ui.AbstractLauncherUiTest; +import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener; +import com.android.quickstep.fallback.window.RecentsWindowFlags; +import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.RecentsViewContainer; import org.junit.After; import org.junit.Before; @@ -107,6 +111,9 @@ public class FallbackRecentsTest { @Rule public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule(); + @Rule(order = -1000) // This should be the outermost rule + public SkipAfterTimeOutRule mSkipAfterTimeOutRule = new SkipAfterTimeOutRule(); + public FallbackRecentsTest() throws RemoteException { Instrumentation instrumentation = getInstrumentation(); Context context = instrumentation.getContext(); @@ -129,6 +136,7 @@ public class FallbackRecentsTest { getLauncherCommand(mOtherLauncherActivity)); updateHandler.mChangeCounter .await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS); + mLauncher.setTestLauncherPackage(mOtherLauncherActivity.packageName); try { base.evaluate(); } finally { @@ -136,19 +144,17 @@ public class FallbackRecentsTest { TestCommandReceiver.callCommand(TestCommandReceiver.DISABLE_TEST_LAUNCHER); UiDevice.getInstance(getInstrumentation()).executeShellCommand( getLauncherCommand(getLauncherInMyProcess())); + mLauncher.setTestLauncherPackage(null); pressHomeAndWaitForOverviewClose(); } } }; - final ViewCaptureRule viewCaptureRule = new ViewCaptureRule( - RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity); mOrderSensitiveRules = RuleChain .outerRule(new SamplerRule()) .around(new TestStabilityRule()) .around(new NavigationModeSwitchRule(mLauncher)) - .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData)) - // .around(viewCaptureRule) b/315482167 + .around(new FailureWatcher(mLauncher)) .around(new TestIsolationRule(mLauncher, false)) .around(setLauncherCommand); @@ -192,29 +198,32 @@ public class FallbackRecentsTest { @Test public void goToOverviewFromApp() { startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); - waitForRecentsActivityStop(); + waitForRecentsClosed(); mLauncher.getLaunchedAppState().switchToOverview(); } - protected void executeOnRecents(Consumer f) { + protected void executeOnRecents(Consumer f) { getFromRecents(r -> { f.accept(r); return true; }); } - protected T getFromRecents(Function f) { + protected T getFromRecents(Function f) { if (!TestHelpers.isInLauncherProcess()) return null; Object[] result = new Object[1]; Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> { - RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity(); - if (activity == null) { + RecentsViewContainer recentsViewContainer = + RecentsWindowFlags.enableFallbackOverviewInWindow.isTrue() + ? RecentsWindowManager.getRecentsWindowTracker().getCreatedContext() + : RecentsActivity.ACTIVITY_TRACKER.getCreatedContext(); + if (recentsViewContainer == null) { return false; } - result[0] = f.apply(activity); + result[0] = f.apply(recentsViewContainer); return true; - }).get(), DEFAULT_UI_TIMEOUT, mLauncher); + }).get(), mLauncher); return (T) result[0]; } @@ -225,14 +234,20 @@ public class FallbackRecentsTest { private void pressHomeAndWaitForOverviewClose() { mDevice.pressHome(); - waitForRecentsActivityStop(); + waitForRecentsClosed(); } - private void waitForRecentsActivityStop() { + private void waitForRecentsClosed() { try { - final boolean recentsActivityIsNull = MAIN_EXECUTOR.submit( - () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity() == null).get(); - if (recentsActivityIsNull) { + final boolean isRecentsContainerNUll = MAIN_EXECUTOR.submit(() -> { + RecentsViewContainer recentsViewContainer = + RecentsWindowFlags.enableFallbackOverviewInWindow.isTrue() + ? RecentsWindowManager.getRecentsWindowTracker().getCreatedContext() + : RecentsActivity.ACTIVITY_TRACKER.getCreatedContext(); + + return recentsViewContainer == null; + }).get(); + if (isRecentsContainerNUll) { // Null activity counts as a "stopped" one. return; } @@ -242,9 +257,9 @@ public class FallbackRecentsTest { throw new RuntimeException(e); } - Wait.atMost("Recents activity didn't stop", + Wait.atMost("Recents view container didn't close", () -> getFromRecents(recents -> !recents.isStarted()), - DEFAULT_UI_TIMEOUT, mLauncher); + mLauncher); } @Test @@ -252,9 +267,10 @@ public class FallbackRecentsTest { startAppFast(getAppPackageName()); startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)); startTestActivity(2); - waitForRecentsActivityStop(); + waitForRecentsClosed(); Wait.atMost("Expected three apps in the task list", - () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher); + () -> mLauncher.getRecentTasks().size() >= 3, + mLauncher); checkTestLauncher(); BaseOverview overview = mLauncher.getLaunchedAppState().switchToOverview(); @@ -282,7 +298,7 @@ public class FallbackRecentsTest { assertNotNull("OverviewTask.open returned null", task.open()); assertTrue("Test activity didn't open from Overview", TestHelpers.wait(Until.hasObject( By.pkg(getAppPackageName()).text("TestActivity2")), - DEFAULT_UI_TIMEOUT)); + TestUtil.DEFAULT_UI_TIMEOUT)); // Test dismissing a task. @@ -312,37 +328,39 @@ public class FallbackRecentsTest { ); } - private int getCurrentOverviewPage(RecentsActivity recents) { - return recents.getOverviewPanel().getCurrentPage(); + private int getCurrentOverviewPage(RecentsViewContainer recentsViewContainer) { + return recentsViewContainer.getOverviewPanel().getCurrentPage(); } - private int getTaskCount(RecentsActivity recents) { - return recents.getOverviewPanel().getTaskViewCount(); + private int getTaskCount(RecentsViewContainer recentsViewContainer) { + return recentsViewContainer.getOverviewPanel().getTaskViewCount(); } - private class OverviewUpdateHandler { + private class OverviewUpdateHandler implements OverviewChangeListener { - final RecentsAnimationDeviceState mRads; final OverviewComponentObserver mObserver; final CountDownLatch mChangeCounter; OverviewUpdateHandler() { Context ctx = getInstrumentation().getTargetContext(); - mRads = new RecentsAnimationDeviceState(ctx); - mObserver = new OverviewComponentObserver(ctx, mRads); + mObserver = OverviewComponentObserver.INSTANCE.get(ctx); mChangeCounter = new CountDownLatch(1); - if (mObserver.getHomeIntent().getComponent() + if (mObserver.getHomeIntent(DEFAULT_DISPLAY).getComponent() .getPackageName().equals(mOtherLauncherActivity.packageName)) { // Home already same mChangeCounter.countDown(); } else { - mObserver.setOverviewChangeListener(b -> mChangeCounter.countDown()); + mObserver.addOverviewChangeListener(this); } } + @Override + public void onOverviewTargetChange(boolean isHomeAndOverviewSame) { + mChangeCounter.countDown(); + } + void destroy() { - mObserver.onDestroy(); - mRads.destroy(); + mObserver.removeOverviewChangeListener(this); } } } diff --git a/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java new file mode 100644 index 0000000000..bc34471acf --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java @@ -0,0 +1,641 @@ +/* + * 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.quickstep; + +import static com.android.quickstep.InputConsumerUtils.newBaseConsumer; +import static com.android.quickstep.InputConsumerUtils.newConsumer; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.StubberKt.doCallRealMethod; + +import android.annotation.NonNull; +import android.os.Looper; +import android.view.Choreographer; +import android.view.Display; +import android.view.MotionEvent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.dagger.LauncherAppComponent; +import com.android.launcher3.dagger.LauncherAppModule; +import com.android.launcher3.dagger.LauncherAppSingleton; +import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.launcher3.taskbar.TaskbarManager; +import com.android.launcher3.taskbar.bubbles.BubbleBarController; +import com.android.launcher3.taskbar.bubbles.BubbleBarPinController; +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; +import com.android.launcher3.taskbar.bubbles.BubbleControllers; +import com.android.launcher3.taskbar.bubbles.BubbleCreator; +import com.android.launcher3.taskbar.bubbles.BubbleDismissController; +import com.android.launcher3.taskbar.bubbles.BubbleDragController; +import com.android.launcher3.taskbar.bubbles.BubblePinController; +import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController; +import com.android.launcher3.taskbar.bubbles.DragToBubbleController; +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; +import com.android.launcher3.util.LockedUserState; +import com.android.launcher3.util.SandboxApplication; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.inputconsumers.AccessibilityInputConsumer; +import com.android.quickstep.inputconsumers.BubbleBarInputConsumer; +import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer; +import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer; +import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer; +import com.android.quickstep.inputconsumers.OtherActivityInputConsumer; +import com.android.quickstep.inputconsumers.OverviewInputConsumer; +import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer; +import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer; +import com.android.quickstep.inputconsumers.ResetGestureInputConsumer; +import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer; +import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer; +import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer; +import com.android.quickstep.util.ActiveGestureLog; +import com.android.quickstep.util.NavBarPosition; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.shared.system.InputMonitorCompat; + +import dagger.BindsInstance; +import dagger.Component; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Optional; +import java.util.function.Function; + +import javax.inject.Provider; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class InputConsumerUtilsTest { + + @Rule public final SandboxApplication mContext = new SandboxApplication(); + + private final int mDisplayId = Display.DEFAULT_DISPLAY; + @NonNull private final InputMonitorCompat mInputMonitorCompat = + new InputMonitorCompat("", mDisplayId); + + private TaskAnimationManager mTaskAnimationManager; + private InputChannelCompat.InputEventReceiver mInputEventReceiver; + private boolean mUserUnlocked = true; + @NonNull private Function mSwipeUpProxyProvider = (state) -> null; + + @NonNull @Mock private TaskbarActivityContext mTaskbarActivityContext; + @NonNull @Mock private OverviewComponentObserver mOverviewComponentObserver; + @NonNull @Mock private RecentsAnimationDeviceState mDeviceState; + @NonNull @Mock private RotationTouchHelper mRotationTouchHelper; + @NonNull @Mock private AbsSwipeUpHandler.Factory mSwipeUpHandlerFactory; + @NonNull @Mock private TaskbarManager mTaskbarManager; + @NonNull @Mock private OverviewCommandHelper mOverviewCommandHelper; + @NonNull @Mock private GestureState mPreviousGestureState; + @NonNull @Mock private GestureState mCurrentGestureState; + @NonNull @Mock private LockedUserState mLockedUserState; + @NonNull @Mock private TopTaskTracker.CachedTaskInfo mRunningTask; + @NonNull @Mock private BaseContainerInterface mContainerInterface; + @NonNull @Mock private BaseDragLayer mBaseDragLayer; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Before + public void setupTaskAnimationManager() { + mTaskAnimationManager = new TaskAnimationManager(mContext, mDisplayId); + } + + @Before + public void setupDaggerGraphOverrides() { + mContext.initDaggerComponent(DaggerInputConsumerUtilsTest_TestComponent + .builder() + .bindLockedState(mLockedUserState) + ); + } + + @Before + public void setUpInputEventReceiver() { + runOnMainSync(() -> + mInputEventReceiver = mInputMonitorCompat.getInputReceiver( + Looper.getMainLooper(), + Choreographer.getInstance(), + event -> {})); + } + + @Before + public void setUpTaskbarActivityContext() { + NavHandle navHandle = mock(NavHandle.class); + + when(navHandle.canNavHandleBeLongPressed()).thenReturn(true); + + when(mTaskbarActivityContext.getDeviceProfile()).thenReturn(new DeviceProfile()); + when(mTaskbarActivityContext.getNavHandle()).thenReturn(navHandle); + } + + @Before + public void setUpTaskbarManager() { + when(mTaskbarManager.getCurrentActivityContext()).thenReturn(mTaskbarActivityContext); + } + + @Before + public void setupLockedUserState() { + when(mLockedUserState.isUserUnlocked()).thenReturn(true); + } + + @Before + public void setupGestureStates() { + when(mCurrentGestureState.getRunningTask()).thenReturn(mRunningTask); + doReturn(mContainerInterface).when(mCurrentGestureState).getContainerInterface(); + } + + @Before + public void setUpContainerInterface() { + RecentsViewContainer recentsViewContainer = mock(RecentsViewContainer.class); + + when(recentsViewContainer.getDragLayer()).thenReturn(mBaseDragLayer); + when(recentsViewContainer.getRootView()).thenReturn(mBaseDragLayer); + when(recentsViewContainer.asContext()).thenReturn(mContext); + + doReturn(recentsViewContainer).when(mContainerInterface).getCreatedContainer(); + } + + @Before + public void setupBaseDragLayer() { + when(mBaseDragLayer.hasWindowFocus()).thenReturn(true); + } + + @Before + public void setupDeviceState() { + when(mDeviceState.canStartTrackpadGesture()).thenReturn(true); + when(mDeviceState.canStartSystemGesture()).thenReturn(true); + when(mDeviceState.isFullyGesturalNavMode()).thenReturn(true); + when(mDeviceState.getNavBarPosition()).thenReturn(mock(NavBarPosition.class)); + } + + @After + public void cleanUp() { + runOnMainSync(() -> { + mInputMonitorCompat.dispose(); + mInputEventReceiver.dispose(); + }); + } + + @Test + public void testNewBaseConsumer_onKeyguard_returnsDeviceLockedInputConsumer() { + when(mDeviceState.isKeyguardShowingOccluded()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + DeviceLockedInputConsumer.class, + InputConsumer.TYPE_DEVICE_LOCKED); + } + + @Test + public void testNewBaseConsumer_onLiveTileModeWithNoContainer_returnsDefaultInputConsumer() { + when(mContainerInterface.isInLiveTileMode()).thenReturn(true); + when(mContainerInterface.getCreatedContainer()).thenReturn(null); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_onLiveTileMode_returnsOverviewInputConsumer() { + when(mContainerInterface.isInLiveTileMode()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_withNoRunningTask_returnsDefaultInputConsumer() { + when(mCurrentGestureState.getRunningTask()).thenReturn(null); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_prevGestureAnimatingToLauncher_returnsOverviewInputConsumer() { + when(mPreviousGestureState.isRunningAnimationToLauncher()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_predictiveBackToHomeInProgress_returnsOverviewInputConsumer() { + when(mDeviceState.isPredictiveBackToHomeInProgress()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_resumedThroughShellTransition_returnsOverviewInputConsumer() { + when(mContainerInterface.isResumed()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_shellNoWindowFocus_returnsOverviewWithoutFocusInputConsumer() { + when(mContainerInterface.isResumed()).thenReturn(true); + when(mBaseDragLayer.hasWindowFocus()).thenReturn(false); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewWithoutFocusInputConsumer.class, + InputConsumer.TYPE_OVERVIEW_WITHOUT_FOCUS); + } + + @Test + public void testNewBaseConsumer_forceOverviewInputConsumer_returnsOverviewInputConsumer() { + when(mContainerInterface.isResumed()).thenReturn(true); + when(mRunningTask.isRootChooseActivity()).thenReturn(true); + + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OverviewInputConsumer.class, + InputConsumer.TYPE_OVERVIEW); + } + + @Test + public void testNewBaseConsumer_launcherChildActivityResumed_returnsDefaultInputConsumer() { + when(mRunningTask.isHomeTask()).thenReturn(true); + when(mOverviewComponentObserver.isHomeAndOverviewSame()).thenReturn(true); + when(mContainerInterface.isLauncherOverlayShowing()).thenReturn(true); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_onGestureBlockedTask_returnsDefaultInputConsumer() { + when(mDeviceState.isGestureBlockedTask(any())).thenReturn(true); + + assertEqualsDefaultInputConsumer(this::createBaseInputConsumer); + } + + @Test + public void testNewBaseConsumer_noGestureBlockedTask_returnsOtherActivityInputConsumer() { + doCallRealMethod().when(mDeviceState).setGestureBlockingTaskId(anyInt()); + mDeviceState.setGestureBlockingTaskId(-1); + when(mDeviceState.isGestureBlockedTask(any())).thenCallRealMethod(); + + assertCorrectInputConsumer(this::createBaseInputConsumer, OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY); + } + + @Test + public void testNewBaseConsumer_containsOtherActivityInputConsumer() { + assertCorrectInputConsumer( + this::createBaseInputConsumer, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY); + } + + @Test + public void testNewConsumer_containsOtherActivityInputConsumer() { + assertCorrectInputConsumer( + this::createInputConsumer, + NavHandleLongPressInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS); + } + + @Test + public void testNewConsumer_eventCanTriggerAssistantAction_containsAssistantInputConsumer() { + when(mDeviceState.canTriggerAssistantAction(any())).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + NavHandleLongPressInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_ASSISTANT); + } + + @Test + public void testNewConsumer_taskbarIsPresent_containsTaskbarUnstashInputConsumer() { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.isTaskbarPresent = true; + when(mTaskbarActivityContext.getDeviceProfile()).thenReturn(deviceProfile); + + assertCorrectInputConsumer( + this::createInputConsumer, + NavHandleLongPressInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_TASKBAR_STASH + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_CURSOR_HOVER); + } + + @Test + public void testNewConsumer_whileSystemUiDialogShowing_returnsSysUiOverlayInputConsumer() { + when(mDeviceState.isSystemUiDialogShowing()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + SysUiOverlayInputConsumer.class, + InputConsumer.TYPE_SYSUI_OVERLAY); + } + + @Test + public void testNewConsumer_onTrackpadGesture_returnsTrackpadStatusBarInputConsumer() { + when(mCurrentGestureState.isTrackpadGesture()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + TrackpadStatusBarInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_STATUS_BAR); + } + + @Test + public void testNewConsumer_whileScreenPinningActive_returnsScreenPinnedInputConsumer() { + when(mDeviceState.isScreenPinningActive()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + ScreenPinnedInputConsumer.class, + InputConsumer.TYPE_SCREEN_PINNED); + } + + @Test + public void testNewConsumer_canTriggerOneHandedAction_returnsOneHandedModeInputConsumer() { + when(mDeviceState.canTriggerOneHandedAction(any())).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + OneHandedModeInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_ONE_HANDED); + } + + @Test + public void testNewConsumer_accessibilityMenuAvailable_returnsAccessibilityInputConsumer() { + when(mDeviceState.isAccessibilityMenuAvailable()).thenReturn(true); + + assertCorrectInputConsumer( + this::createInputConsumer, + AccessibilityInputConsumer.class, + OtherActivityInputConsumer.class, + InputConsumer.TYPE_OTHER_ACTIVITY + | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS + | InputConsumer.TYPE_ACCESSIBILITY); + } + + @Test + public void testNewConsumer_onStashedBubbleBar_returnsBubbleBarInputConsumer() { + BubbleControllers bubbleControllers = createBubbleControllers(/* isStashed= */ true); + + when(mTaskbarActivityContext.isBubbleBarEnabled()).thenReturn(true); + when(mTaskbarActivityContext.getBubbleControllers()).thenReturn(bubbleControllers); + + assertCorrectInputConsumer( + this::createInputConsumer, + BubbleBarInputConsumer.class, + InputConsumer.TYPE_BUBBLE_BAR); + } + + @Test + public void testNewConsumer_onVisibleBubbleBar_returnsBubbleBarInputConsumer() { + BubbleControllers bubbleControllers = createBubbleControllers(/* isStashed= */ false); + + when(mTaskbarActivityContext.isBubbleBarEnabled()).thenReturn(true); + when(mTaskbarActivityContext.getBubbleControllers()).thenReturn(bubbleControllers); + + assertCorrectInputConsumer( + this::createInputConsumer, + BubbleBarInputConsumer.class, + InputConsumer.TYPE_BUBBLE_BAR); + } + + @Test + public void testNewConsumer_withSwipeUpProxyProvider_returnsProgressDelegateInputConsumer() { + mSwipeUpProxyProvider = (state) -> new AnimatedFloat(); + + assertCorrectInputConsumer( + this::createInputConsumer, + ProgressDelegateInputConsumer.class, + InputConsumer.TYPE_PROGRESS_DELEGATE); + } + + @Test + public void testNewConsumer_onLockedState_returnsDeviceLockedInputConsumer() { + when(mLockedUserState.isUserUnlocked()).thenReturn(false); + + assertCorrectInputConsumer( + this::createInputConsumer, + DeviceLockedInputConsumer.class, + InputConsumer.TYPE_DEVICE_LOCKED); + } + + @Test + public void testNewConsumer_cannotStartSysGestureOnLockedState_returnsDefaultInputConsumer() { + when(mLockedUserState.isUserUnlocked()).thenReturn(false); + when(mDeviceState.canStartSystemGesture()).thenReturn(false); + + assertEqualsDefaultInputConsumer(this::createInputConsumer); + } + + @Test + public void testNewConsumer_cannotStartTrackGestureOnLockedState_returnsDefaultInputConsumer() { + when(mLockedUserState.isUserUnlocked()).thenReturn(false); + when(mCurrentGestureState.isTrackpadGesture()).thenReturn(true); + when(mDeviceState.canStartTrackpadGesture()).thenReturn(false); + + assertEqualsDefaultInputConsumer(this::createInputConsumer); + } + + private InputConsumer createInputConsumer() { + MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + InputConsumer inputConsumer = newConsumer( + mContext, + mUserUnlocked, + mOverviewComponentObserver, + mDeviceState, + mPreviousGestureState, + mCurrentGestureState, + mTaskAnimationManager, + mInputMonitorCompat, + mSwipeUpHandlerFactory, + otherActivityInputConsumer -> {}, + mInputEventReceiver, + mTaskbarManager, + mSwipeUpProxyProvider, + mOverviewCommandHelper, + event, + mRotationTouchHelper); + + event.recycle(); + + return inputConsumer; + } + + private InputConsumer createBaseInputConsumer() { + MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + InputConsumer inputConsumer = newBaseConsumer( + mContext, + mUserUnlocked, + mTaskbarManager, + mOverviewComponentObserver, + mDeviceState, + mPreviousGestureState, + mCurrentGestureState, + mTaskAnimationManager, + mInputMonitorCompat, + mSwipeUpHandlerFactory, + otherActivityInputConsumer -> {}, + mInputEventReceiver, + event, + ActiveGestureLog.CompoundString.NO_OP, + mRotationTouchHelper); + + event.recycle(); + + return inputConsumer; + } + + private void assertEqualsDefaultInputConsumer( + @NonNull Provider inputConsumerProvider) { + assertCorrectInputConsumer( + inputConsumerProvider, + ResetGestureInputConsumer.class, + InputConsumer.TYPE_RESET_GESTURE); + + mUserUnlocked = false; + + assertCorrectInputConsumer( + inputConsumerProvider, + InputConsumer.class, + InputConsumer.TYPE_NO_OP); + } + + private void assertCorrectInputConsumer( + @NonNull Provider inputConsumerProvider, + @NonNull Class expectedOutputConsumer, + int expectedType) { + assertCorrectInputConsumer( + inputConsumerProvider, + expectedOutputConsumer, + expectedOutputConsumer, + expectedType); + } + + private void assertCorrectInputConsumer( + @NonNull Provider inputConsumerProvider, + @NonNull Class expectedOutputConsumer, + @NonNull Class expectedActiveConsumer, + int expectedType) { + when(mCurrentGestureState.getDisplayId()).thenReturn(mDisplayId); + + runOnMainSync(() -> { + InputConsumer inputConsumer = inputConsumerProvider.get(); + + assertThat(inputConsumer).isInstanceOf(expectedOutputConsumer); + assertThat(inputConsumer.getActiveConsumerInHierarchy()) + .isInstanceOf(expectedActiveConsumer); + assertThat(inputConsumer.getType()).isEqualTo(expectedType); + assertThat(inputConsumer.getDisplayId()).isEqualTo(mDisplayId); + }); + int expectedDisplayId = mDisplayId + 1; + + when(mCurrentGestureState.getDisplayId()).thenReturn(expectedDisplayId); + + runOnMainSync(() -> assertThat(inputConsumerProvider.get().getDisplayId()) + .isEqualTo(expectedDisplayId)); + } + + private static void runOnMainSync(@NonNull Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } + + private static BubbleControllers createBubbleControllers(boolean isStashed) { + BubbleBarController bubbleBarController = mock(BubbleBarController.class); + BubbleBarViewController bubbleBarViewController = mock(BubbleBarViewController.class); + BubbleStashController bubbleStashController = mock(BubbleStashController.class); + BubbleStashedHandleViewController bubbleStashedHandleViewController = + mock(BubbleStashedHandleViewController.class); + BubbleDragController bubbleDragController = mock(BubbleDragController.class); + BubbleDismissController bubbleDismissController = mock(BubbleDismissController.class); + BubbleBarPinController bubbleBarPinController = mock(BubbleBarPinController.class); + BubblePinController bubblePinController = mock(BubblePinController.class); + BubbleBarSwipeController bubbleBarSwipeController = mock(BubbleBarSwipeController.class); + DragToBubbleController dragToBubbleController = mock(DragToBubbleController.class); + BubbleCreator bubbleCreator = mock(BubbleCreator.class); + BubbleControllers bubbleControllers = new BubbleControllers( + bubbleBarController, + bubbleBarViewController, + bubbleStashController, + Optional.of(bubbleStashedHandleViewController), + bubbleDragController, + bubbleDismissController, + bubbleBarPinController, + bubblePinController, + Optional.of(bubbleBarSwipeController), + dragToBubbleController, + bubbleCreator); + + when(bubbleBarViewController.hasBubbles()).thenReturn(true); + when(bubbleStashController.isStashed()).thenReturn(isStashed); + when(bubbleStashedHandleViewController.isEventOverHandle(any())).thenReturn(true); + when(bubbleBarViewController.isBubbleBarVisible()).thenReturn(!isStashed); + when(bubbleBarViewController.isEventOverBubbleBar(any())).thenReturn(true); + + return bubbleControllers; + } + + @LauncherAppSingleton + @Component(modules = {LauncherAppModule.class}) + interface TestComponent extends LauncherAppComponent { + @Component.Builder + interface Builder extends LauncherAppComponent.Builder { + @BindsInstance Builder bindLockedState(LockedUserState state); + + @Override + TestComponent build(); + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java index 4459ed6944..212f933798 100644 --- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java +++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java @@ -16,7 +16,7 @@ package com.android.quickstep; -import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ALL; import static com.android.quickstep.NavigationModeSwitchRule.Mode.THREE_BUTTON; @@ -26,16 +26,17 @@ import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_ import android.content.Context; import android.content.pm.PackageManager; +import android.os.Process; import android.util.Log; import androidx.test.uiautomator.UiDevice; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.TestHelpers; -import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.FailureWatcher; +import com.android.launcher3.util.ui.AbstractLauncherUiTest; import com.android.systemui.shared.system.QuickStepContract; import org.junit.rules.TestRule; @@ -57,8 +58,6 @@ public class NavigationModeSwitchRule implements TestRule { static final String TAG = "QuickStepOnOffRule"; - public static final int WAIT_TIME_MS = 10000; - public enum Mode { THREE_BUTTON, ZERO_BUTTON, ALL } @@ -155,7 +154,9 @@ public class NavigationModeSwitchRule implements TestRule { Log.d(TAG, "setActiveOverlay: " + overlayPackage + "..."); UiDevice.getInstance(getInstrumentation()).executeShellCommand( - "cmd overlay enable-exclusive --category " + overlayPackage); + String.format("cmd overlay enable-exclusive --user %d --category %s", + Process.myUserHandle().getIdentifier(), + overlayPackage)); if (currentSysUiNavigationMode() != expectedMode) { final CountDownLatch latch = new CountDownLatch(1); @@ -179,12 +180,13 @@ public class NavigationModeSwitchRule implements TestRule { } Wait.atMost("Couldn't switch to " + overlayPackage, - () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher); + () -> launcher.getNavigationModel() == expectedMode, + launcher); Wait.atMost(() -> "Switching nav mode: " + launcher.getNavigationModeMismatchError(false), () -> launcher.getNavigationModeMismatchError(false) == null, - WAIT_TIME_MS, launcher); + launcher); AbstractLauncherUiTest.checkDetectedLeaks(launcher, false); return true; } @@ -203,12 +205,16 @@ public class NavigationModeSwitchRule implements TestRule { private static void assertTrue(LauncherInstrumentation launcher, String message, boolean condition, Description description) { - launcher.checkForAnomaly(true, true); - if (!condition) { + try { + launcher.checkForAnomaly(true, true); + if (!condition) { + throw new AssertionError(message); + } + } catch (Throwable e) { if (description != null) { FailureWatcher.onError(launcher, description); } - throw new AssertionError(message); + throw e; } } } diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java index a738e76833..cd8dd6e00c 100644 --- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java +++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java @@ -21,6 +21,7 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.android.launcher3.util.NavigationMode.NO_BUTTON; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -41,6 +42,8 @@ import android.view.Surface; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.launcher3.testing.shared.ResourceUtils; +import com.android.launcher3.util.DaggerSingletonTracker; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.RotationUtils; import com.android.launcher3.util.WindowBounds; @@ -88,7 +91,7 @@ public class OrientationTouchTransformerTest { float landscapeRegionY = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertFalse(100, landscapeRegionY, @@ -100,7 +103,8 @@ public class OrientationTouchTransformerTest { // Override region mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); tapAndAssertFalse(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertTrue(100, landscapeRegionY, @@ -111,7 +115,7 @@ public class OrientationTouchTransformerTest { event -> mTouchTransformer.touchInAssistantRegion(event)); // Override region again - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertFalse(100, landscapeRegionY, @@ -130,7 +134,8 @@ public class OrientationTouchTransformerTest { generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); tapAndAssertFalse(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); tapAndAssertTrue(100, landscapeRegionY, @@ -142,7 +147,7 @@ public class OrientationTouchTransformerTest { // We have to add 0 rotation second so that gets set as the current rotation, otherwise // matrix transform will fail (tests only work in Portrait at the moment) mTouchTransformer.enableMultipleRegions(true, mInfo); - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(100, portraitRegionY, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -163,8 +168,9 @@ public class OrientationTouchTransformerTest { mTouchTransformer.enableMultipleRegions(true, mInfo); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); - mTouchTransformer.createOrAddTouchRegion(mInfo); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertTrue(0, portraitRegionY, event -> mTouchTransformer.touchInAssistantRegion(event)); tapAndAssertFalse(0, landscapeRegionY, @@ -179,9 +185,10 @@ public class OrientationTouchTransformerTest { generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; mTouchTransformer.enableMultipleRegions(true, mInfo); - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); mTouchTransformer.enableMultipleRegions(false, mInfo); tapAndAssertTrue(0, portraitRegionY, event -> mTouchTransformer.touchInAssistantRegion(event)); @@ -211,14 +218,14 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_notInRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); tapAndAssertFalse(100, 100, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); } @Test public void applyTransform_taskFrozen_noRotate_outOfRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); tapAndAssertFalse(100, 100, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -226,7 +233,7 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskFrozen_noRotate_inRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1; tapAndAssertTrue(100, y, @@ -235,7 +242,7 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_noRotate_inDefaultRegion() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1; tapAndAssertTrue(100, y, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -244,7 +251,8 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_90Rotate_inRegion() { mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; tapAndAssertTrue(100, y, event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY())); @@ -252,10 +260,11 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_taskNotFrozen_90Rotate_withTwoRegions() { - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); // Landscape point float y1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1; MotionEvent inRegion1_down = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, y1); @@ -276,10 +285,11 @@ public class OrientationTouchTransformerTest { @Test public void applyTransform_90Rotate_inRotatedRegion() { // Create regions for both 0 Rotation and 90 Rotation - mTouchTransformer.createOrAddTouchRegion(mInfo); + mTouchTransformer.createOrAddTouchRegion(mInfo, "test"); mTouchTransformer.enableMultipleRegions(true, mInfo); mTouchTransformer - .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90)); + .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90), + "test"); // Portrait point in landscape orientation axis float x1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0); // bottom of screen, from landscape perspective right side of screen @@ -287,6 +297,35 @@ public class OrientationTouchTransformerTest { assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY())); } + @Test + public void testSimpleOrientationTouchTransformer() { + final DisplayController displayController = mock(DisplayController.class); + doReturn(mInfo).when(displayController).getInfoForDisplay(anyInt()); + final SimpleOrientationTouchTransformer transformer = + new SimpleOrientationTouchTransformer(getApplicationContext(), displayController, + mock(DaggerSingletonTracker.class)); + final MotionEvent move1 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10); + transformer.transform(move1, Surface.ROTATION_90); + // The position is transformed to 90 degree. + assertEquals(10, move1.getX(), 0f /* delta */); + assertEquals(NORMAL_SCREEN_SIZE.getWidth() - 100, move1.getY(), 0f /* delta */); + + // If the touching state is specified, the position is still transformed to 90 degree even + // if the given rotation is changed. + final MotionEvent move2 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10); + transformer.updateTouchingOrientation(Surface.ROTATION_90); + transformer.transform(move2, Surface.ROTATION_0); + assertEquals(move1.getX(), move2.getX(), 0f /* delta */); + assertEquals(move1.getY(), move2.getY(), 0f /* delta */); + + // If the touching state is cleared, it restores to use the given rotation. + final MotionEvent move3 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10); + transformer.clearTouchingOrientation(); + transformer.transform(move3, Surface.ROTATION_0); + assertEquals(100, move3.getX(), 0f /* delta */); + assertEquals(10, move3.getY(), 0f /* delta */); + } + private DisplayController.Info createDisplayInfo(Size screenSize, int rotation) { Point displaySize = new Point(screenSize.getWidth(), screenSize.getHeight()); RotationUtils.rotateSize(displaySize, rotation); @@ -300,7 +339,8 @@ public class OrientationTouchTransformerTest { ArrayMap> internalDisplayBounds = new ArrayMap<>(); doReturn(internalDisplayBounds).when(wmProxy).estimateInternalDisplayBounds(any()); return new DisplayController.Info( - getApplicationContext(), wmProxy, new ArrayMap<>()); + getApplicationContext(), false, wmProxy, new ArrayMap<>(), + DisplayMetrics.DENSITY_DEFAULT); } private float generateTouchRegionHeight(Size screenSize, int rotation) { diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java deleted file mode 100644 index 03244eb0bf..0000000000 --- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.quickstep; - -import static junit.framework.TestCase.assertNull; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.app.KeyguardManager; - -import androidx.test.filters.SmallTest; - -import com.android.launcher3.util.LooperExecutor; -import com.android.quickstep.util.GroupTask; -import com.android.wm.shell.util.GroupedRecentTaskInfo; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -@SmallTest -public class RecentTasksListTest { - - @Mock - private SystemUiProxy mockSystemUiProxy; - @Mock - private TopTaskTracker mTopTaskTracker; - - // Class under test - private RecentTasksList mRecentTasksList; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class); - KeyguardManager mockKeyguardManager = mock(KeyguardManager.class); - mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager, - mockSystemUiProxy, mTopTaskTracker); - } - - @Test - public void onRecentTasksChanged_doesNotFetchTasks() { - mRecentTasksList.onRecentTasksChanged(); - verify(mockSystemUiProxy, times(0)) - .getRecentTasks(anyInt(), anyInt()); - } - - @Test - public void loadTasksInBackground_onlyKeys_noValidTaskDescription() { - GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks( - new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null); - when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) - .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); - - List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, - true); - - assertEquals(1, taskList.size()); - assertNull(taskList.get(0).task1.taskDescription.getLabel()); - assertNull(taskList.get(0).task2.taskDescription.getLabel()); - } - - @Test - public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() { - String taskDescription = "Wheeee!"; - ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo(); - task1.taskDescription = new ActivityManager.TaskDescription(taskDescription); - ActivityManager.RecentTaskInfo task2 = new ActivityManager.RecentTaskInfo(); - task2.taskDescription = new ActivityManager.TaskDescription(); - GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2, - null); - when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt())) - .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos))); - - List taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1, - false); - - assertEquals(1, taskList.size()); - assertEquals(taskDescription, taskList.get(0).task1.taskDescription.getLabel()); - assertNull(taskList.get(0).task2.taskDescription.getLabel()); - } -} diff --git a/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt new file mode 100644 index 0000000000..418d66c616 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/RecentsDeviceProfileRepositoryImplTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.launcher3.FakeInvariantDeviceProfileTest +import com.android.quickstep.recents.data.RecentsDeviceProfile +import com.android.quickstep.recents.data.RecentsDeviceProfileRepositoryImpl +import com.android.quickstep.views.RecentsViewContainer +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +class RecentsDeviceProfileRepositoryImplTest : FakeInvariantDeviceProfileTest() { + private val recentsViewContainer: RecentsViewContainer = mock() + + private lateinit var mockitoSession: StaticMockitoSession + + @Before + override fun setUp() { + super.setUp() + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus::class.java) + .startMocking() + whenever(recentsViewContainer.asContext()).thenReturn(context) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + fun deviceProfileMappedCorrectlyForPhone() { + val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer) + initializeVarsForPhone() + val phoneDeviceProfile = newDP() + whenever(recentsViewContainer.deviceProfile).thenReturn(phoneDeviceProfile) + + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) + assertThat(deviceProfileRepo.getRecentsDeviceProfile()) + .isEqualTo(RecentsDeviceProfile(isLargeScreen = false, canEnterDesktopMode = false)) + } + + @Test + fun deviceProfileMappedCorrectlyForTablet() { + val deviceProfileRepo = RecentsDeviceProfileRepositoryImpl(recentsViewContainer) + initializeVarsForTablet() + val tabletDeviceProfile = newDP() + whenever(recentsViewContainer.deviceProfile).thenReturn(tabletDeviceProfile) + + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + assertThat(deviceProfileRepo.getRecentsDeviceProfile()) + .isEqualTo(RecentsDeviceProfile(isLargeScreen = true, canEnterDesktopMode = true)) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java index b7fd8be311..7aa4fb65f9 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java +++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java @@ -15,20 +15,16 @@ */ package com.android.quickstep; -import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; -import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; - import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.content.Intent; import android.platform.test.annotations.PlatinumTest; -import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask; +import com.android.launcher3.tapl.Overview; +import com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer; import com.android.launcher3.tapl.OverviewTaskMenu; -import com.android.launcher3.ui.AbstractLauncherUiTest; -import com.android.launcher3.uioverrides.QuickstepLauncher; -import com.android.launcher3.util.rule.TestStabilityRule; +import com.android.quickstep.util.SplitScreenTestUtils; import org.junit.Test; @@ -36,7 +32,7 @@ import org.junit.Test; * This test run in both Out of process (Oop) and in-process (Ipc). * Tests the app Icon in overview. */ -public class TaplOverviewIconTest extends AbstractLauncherUiTest { +public class TaplOverviewIconTest extends AbstractQuickStepTest { private static final String CALCULATOR_APP_PACKAGE = resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR); @@ -69,41 +65,18 @@ public class TaplOverviewIconTest extends AbstractLauncherUiTest launcher.getAppsView().resetAndScrollToPrivateSpaceHeader()); HomeAllApps homeAllApps = mLauncher.getAllApps(); diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java index 1886ce671a..2fb08dd9a6 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java +++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java @@ -16,6 +16,8 @@ package com.android.quickstep; +import android.util.Log; + import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; @@ -29,8 +31,14 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest { + public static final String TAG = "TaplStartLauncherViaGestureTests"; + static final int STRESS_REPEAT_COUNT = 10; + private enum TestCase { + TO_HOME, TO_OVERVIEW, + } + @Override @Before public void setUp() throws Exception { @@ -41,28 +49,60 @@ public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest { } @Test - @NavigationModeSwitch + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON) public void testStressPressHome() { - for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) { - // Destroy Launcher activity. - closeLauncherActivity(); - - // The test action. - mLauncher.goHome(); - } + runTest(TestCase.TO_HOME); } @Test - @NavigationModeSwitch + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON) + public void testStressSwipeHome() { + runTest(TestCase.TO_HOME); + } + + @Test + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON) + public void testStressPressOverview() { + runTest(TestCase.TO_OVERVIEW); + } + + @Test + @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON) public void testStressSwipeToOverview() { + runTest(TestCase.TO_OVERVIEW); + } + + private void runTest(TestCase testCase) { + long testStartTime = System.currentTimeMillis(); for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) { + long loopStartTime = System.currentTimeMillis(); // Destroy Launcher activity. closeLauncherActivity(); // The test action. - mLauncher.getLaunchedAppState().switchToOverview(); + switch (testCase) { + case TO_OVERVIEW: + mLauncher.getLaunchedAppState().switchToOverview(); + break; + case TO_HOME: + mLauncher.goHome(); + break; + default: + throw new IllegalStateException("Cannot run test case: " + testCase); + } + Log.d(TAG, "Loop " + (i + 1) + " runtime=" + + (System.currentTimeMillis() - loopStartTime) + "ms"); + } + Log.d(TAG, "Test runtime=" + (System.currentTimeMillis() - testStartTime) + "ms"); + switch (testCase) { + case TO_OVERVIEW: + closeLauncherActivity(); + mLauncher.goHome(); + break; + case TO_HOME: + default: + // No-Op + break; } - closeLauncherActivity(); - mLauncher.goHome(); } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestTaskbarIconDrag.kt b/quickstep/tests/src/com/android/quickstep/TaplTestTaskbarIconDrag.kt new file mode 100644 index 0000000000..a56c82d64f --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestTaskbarIconDrag.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.platform.test.annotations.RequiresFlagsEnabled +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.android.launcher3.tapl.BubbleBar +import com.android.launcher3.util.LauncherLayoutBuilder +import com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME +import com.android.launcher3.util.TestUtil +import com.android.wm.shell.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Test +import org.junit.runner.RunWith + +@LargeTest +@RunWith(AndroidJUnit4::class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_BUBBLE_ANYTHING) +class TaplTestTaskbarIconDrag : AbstractQuickStepTest() { + + private var mLauncherLayout: AutoCloseable? = null + + override fun setUp() { + Assume.assumeTrue(mLauncher.isTablet) + super.setUp() + val layoutBuilder = + LauncherLayoutBuilder() + .atHotseat(0) + .putApp( + "com.google.android.apps.nexuslauncher.tests", + "com.android.launcher3.testcomponent.BaseTestingActivity", + ) + mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, layoutBuilder) + performInitialization() + mLauncher.enableBlockTimeout(true) + } + + @After + fun tearDown() { + mLauncher.enableBlockTimeout(false) + mLauncherLayout?.close() + } + + @Test + fun testAppIconDragOnOverviewFromTaskBarToBubbleBar() { + val overview = mLauncher.workspace.switchToOverview() + // test left drop target + overview.taskbar!! + .getAppIcon(TEST_APP_NAME) + .dragToBubbleBarLocation(/* isBubbleBarLeftDropTarget= */ true) + dismissExpandedBubbleBar(overview.bubbleBar) + } + + @Test + fun testAppIconDragInRunningAppFromTaskBarToBubbleBar() { + startAppFast(AbstractTaplTestsTaskbar.CALCULATOR_APP_PACKAGE) + val launchedAppState = mLauncher.launchedAppState + mLauncher.showTaskbarIfHidden() + // test right drop target + launchedAppState.taskbar + .getAppIcon(TEST_APP_NAME) + .dragToBubbleBarLocation(/* isBubbleBarLeftDropTarget= */ false) + // close expanded bubble + dismissExpandedBubbleBar(launchedAppState.bubbleBar) + } + + private fun dismissExpandedBubbleBar(bubbleBar: BubbleBar) { + // close expanded bubble bar + mLauncher.pressBack() + bubbleBar.verifyCollapsed() + // at this moment the bubble bar will be hidden, so need to show it again + mLauncher.showBubbleBarIfHidden() + // dismiss bubble bar + bubbleBar.dragToDismiss() + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java index 43ebb1752a..210c94d8c6 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java @@ -16,11 +16,13 @@ package com.android.quickstep; import android.content.Intent; +import android.platform.test.annotations.DisableFlags; import androidx.annotation.NonNull; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; +import com.android.launcher3.Flags; import com.android.launcher3.tapl.KeyboardQuickSwitch; import com.android.launcher3.taskbar.KeyboardQuickSwitchController; @@ -49,6 +51,7 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { DISMISS(0), LAUNCH_LAST_APP(0), LAUNCH_SELECTED_APP(1), + DISMISS_WHEN_GOING_HOME(1), LAUNCH_OVERVIEW(KeyboardQuickSwitchController.MAX_TASKS - 1); private final int mNumAdditionalRunningTasks; @@ -85,6 +88,7 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { } @Test + @DisableFlags(value = Flags.FLAG_ENABLE_WIDGET_PICKER_REFACTOR) public void testDismiss_fromWidgets() { runTest(TestSurface.WIDGETS, TestCase.DISMISS); } @@ -105,6 +109,7 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { } @Test + @DisableFlags(value = Flags.FLAG_ENABLE_WIDGET_PICKER_REFACTOR) public void testLaunchLastTask_fromWidgets() { runTest(TestSurface.WIDGETS, TestCase.LAUNCH_LAST_APP); } @@ -125,6 +130,7 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { } @Test + @DisableFlags(value = Flags.FLAG_ENABLE_WIDGET_PICKER_REFACTOR) public void testLaunchSelectedTask_fromWidgets() { runTest(TestSurface.WIDGETS, TestCase.LAUNCH_SELECTED_APP); } @@ -145,17 +151,23 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { } @Test + @DisableFlags(value = Flags.FLAG_ENABLE_WIDGET_PICKER_REFACTOR) public void testLaunchOverviewTask_fromWidgets() { runTest(TestSurface.WIDGETS, TestCase.LAUNCH_OVERVIEW); } @Test public void testLaunchSingleRecentTask() { - mLauncher.getLaunchedAppState().switchToOverview().dismissAllTasks(); + clearAllRecentTasks(); startAppFast(CALCULATOR_APP_PACKAGE); mLauncher.goHome().showQuickSwitchView().launchFocusedAppTask(CALCULATOR_APP_PACKAGE); } + @Test + public void testDismissedWhenGoingHome() { + runTest(TestSurface.LAUNCHED_APP, TestCase.DISMISS_WHEN_GOING_HOME); + } + private void runTest(@NonNull TestSurface testSurface, @NonNull TestCase testCase) { for (int i = 0; i < testCase.mNumAdditionalRunningTasks; i++) { startTestActivity(3 + i); @@ -197,6 +209,9 @@ public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest { } kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE); break; + case DISMISS_WHEN_GOING_HOME: + kqs.dismissByGoingHome(); + break; case LAUNCH_OVERVIEW: kqs.moveFocusBackward(); if (!testSurface.mInitialFocusAtZero) { diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java new file mode 100644 index 0000000000..1f0bf35f48 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME; +import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT; +import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.RemoteException; +import android.util.Log; +import android.view.WindowManagerGlobal; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.launcher3.tapl.HomeAllApps; +import com.android.launcher3.util.rule.SetPropRule; +import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; +import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; +import com.android.window.flags.Flags; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; + +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.runner.RunWith; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class TaplTestsLockedTaskbar extends AbstractTaplTestsTaskbar { + private static final String TAG = "TaplTestsLockedTaskbar"; + + @Rule + public SetPropRule mSetPropRule = + new SetPropRule(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP, "true"); + + // Default-to-desktop feature requires the display to be freeform mode. + @Rule + public ExternalResource mFreeformDisplayRule = new ExternalResource() { + private int mOriginalWindowingMode = WINDOWING_MODE_UNDEFINED; + + @Override + protected void before() { + mOriginalWindowingMode = setDisplayWindowingMode(WINDOWING_MODE_FREEFORM); + } + + @Override + protected void after() { + if (mOriginalWindowingMode != WINDOWING_MODE_UNDEFINED) { + setDisplayWindowingMode(mOriginalWindowingMode); + } + } + }; + + @Override + public void setUp() throws Exception { + Assume.assumeTrue(mLauncher.isTablet()); + Assume.assumeTrue(Flags.enterDesktopByDefaultOnFreeformDisplays()); + Assume.assumeTrue(DesktopModeStatus.canEnterDesktopMode(getTargetContext())); + super.setUp(); + } + + @Override + protected boolean startCalendarAppDuringSetup() { + return false; + } + + @Override + protected boolean expectTaskbarIconsMatchHotseat() { + return false; + } + + @Test + @PortraitLandscape + @NavigationModeSwitch + @TaskbarModeSwitch(mode = PERSISTENT) + public void testTaskbarVisibility() { + // The taskbar should be visible on home. + mDevice.pressHome(); + waitForResumed("Launcher internal state is still Background"); + mLauncher.getLaunchedAppState().assertTaskbarVisible(); + + // The taskbar should be visible when a freeform task is active. + startAppFast(CALCULATOR_APP_PACKAGE); + mLauncher.getLaunchedAppState().assertTaskbarVisible(); + } + + @Test + @PortraitLandscape + @NavigationModeSwitch + @TaskbarModeSwitch(mode = PERSISTENT) + public void testDragFromAllAppsToWorspace() { + mDevice.pressHome(); + waitForResumed("Launcher internal state is still Background"); + + final HomeAllApps allApps = getTaskbar().openAllAppsOnHome(); + allApps.freeze(); + try { + allApps.getAppIcon(TEST_APP_NAME).dragToWorkspace(false, false); + assertThat(mLauncher.getWorkspace().getWorkspaceAppIcon(TEST_APP_NAME)).isNotNull(); + } finally { + allApps.unfreeze(); + } + } + + private int setDisplayWindowingMode(int windowingMode) { + try { + int originalWindowingMode = + WindowManagerGlobal.getWindowManagerService().getWindowingMode(DEFAULT_DISPLAY); + WindowManagerGlobal.getWindowManagerService().setWindowingMode( + DEFAULT_DISPLAY, windowingMode); + return originalWindowingMode; + } catch (RemoteException e) { + Log.e(TAG, "error setting windowing mode", e); + throw new RuntimeException(e); + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt new file mode 100644 index 0000000000..d68a50e9e4 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt @@ -0,0 +1,192 @@ +/* + * 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.quickstep + +import android.platform.test.rule.AllowedDevices +import android.platform.test.rule.DeviceProduct +import android.platform.test.rule.IgnoreLimit +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.launcher3.BuildConfig +import com.android.launcher3.tapl.LaunchedAppState +import com.android.launcher3.tapl.OverviewTask +import com.android.launcher3.util.TestUtil +import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Before +import org.junit.Test + +/** Test Desktop windowing in Overview. */ +@AllowedDevices(allowed = [DeviceProduct.CF_TABLET, DeviceProduct.TANGORPRO]) +@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD) +class TaplTestsOverviewDesktop : AbstractQuickStepTest() { + @Before + fun setup() { + clearAllRecentTasks() + startTestAppsWithCheck() + mLauncher.goHome() + } + + @Test + @PortraitLandscape + fun enterDesktopViaOverviewMenu() { + mLauncher.workspace.switchToOverview() + moveTaskToDesktop(TEST_ACTIVITY_2) // Move last launched TEST_ACTIVITY_2 into Desktop + + // Scroll back to TEST_ACTIVITY_1, then move it into Desktop + mLauncher + .goHome() + .switchToOverview() + .apply { flingForward() } + .also { moveTaskToDesktop(TEST_ACTIVITY_1) } + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch static DesktopTaskView without live tile in Overview + val desktopTask = + mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch live-tile DesktopTaskView + desktopTask.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + + // Launch static DesktopTaskView with live tile in Overview + mLauncher.goHome() + startTestActivity(TEST_ACTIVITY_EXTRA) + mLauncher.launchedAppState + .switchToOverview() + .apply { flingBackward() } + .getTestActivityTask(TEST_ACTIVITIES) + .open() + TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) } + } + + @Test + @PortraitLandscape + fun dismissFocusedTasks_thenDesktopIsCentered() { + // Create DesktopTaskView + mLauncher.goHome().switchToOverview() + moveTaskToDesktop(TEST_ACTIVITY_2) + + // Create a new task activity to be the focused task + mLauncher.goHome() + startTestActivity(TEST_ACTIVITY_EXTRA) + + val overview = mLauncher.goHome().switchToOverview() + + // Dismiss focused task + val focusedTask1 = overview.currentTask + assertTaskContentDescription(focusedTask1, TEST_ACTIVITY_EXTRA) + focusedTask1.dismiss() + + // Dismiss new focused task + val focusedTask2 = overview.currentTask + assertTaskContentDescription(focusedTask2, TEST_ACTIVITY_1) + focusedTask2.dismiss() + + // Dismiss DesktopTaskView + val desktopTask = overview.currentTask + assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue() + desktopTask.dismiss() + + assertWithMessage("Still have tasks after dismissing all the tasks") + .that(mLauncher.workspace.switchToOverview().hasTasks()) + .isFalse() + } + + @Test + @PortraitLandscape + fun dismissTasks_whenDesktopTask_IsInTheCenter() { + // Create extra activity to be DesktopTaskView + startTestActivity(TEST_ACTIVITY_EXTRA) + mLauncher.goHome().switchToOverview() + + val desktop = moveTaskToDesktop(TEST_ACTIVITY_EXTRA) + var overview = desktop.switchToOverview() + + // Open first fullscreen task and go back to Overview to validate whether it has adjacent + // tasks in its both sides (grid task on left and desktop tasks at its right side) + val firstFullscreenTaskOpened = overview.getTestActivityTask(TEST_ACTIVITY_2).open() + + // Fling to desktop task and dismiss the first fullscreen task to check repositioning of + // grid tasks. + overview = firstFullscreenTaskOpened.switchToOverview().apply { flingBackward() } + val desktopTask = overview.currentTask + assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue() + + // Get first fullscreen task (previously opened task) then dismiss this task + val firstFullscreenTaskInOverview = overview.getTestActivityTask(TEST_ACTIVITY_2) + assertTaskContentDescription(firstFullscreenTaskInOverview, TEST_ACTIVITY_2) + firstFullscreenTaskInOverview.dismiss() + + // Dismiss DesktopTask to validate whether the new task will take its position + desktopTask.dismiss() + + // Dismiss last fullscreen task + val lastFocusedTask = overview.currentTask + assertTaskContentDescription(lastFocusedTask, TEST_ACTIVITY_1) + lastFocusedTask.dismiss() + + assertWithMessage("Still have tasks after dismissing all the tasks") + .that(mLauncher.workspace.switchToOverview().hasTasks()) + .isFalse() + } + + private fun assertTaskContentDescription(task: OverviewTask, activityIndex: Int) { + assertWithMessage("The current task content description is not TestActivity$activityIndex.") + .that(task.containsContentDescription("TestActivity$activityIndex")) + .isTrue() + } + + private fun moveTaskToDesktop(activityIndex: Int): LaunchedAppState { + return mLauncher.overview + .getTestActivityTask(activityIndex) + .tapMenu() + .tapDesktopMenuItem() + .also { assertTestAppLaunched(activityIndex) } + } + + private fun startTestAppsWithCheck() { + TEST_ACTIVITIES.forEach { + startTestActivity(it) + executeOnLauncher { launcher -> + assertWithMessage( + "Launcher activity is the top activity; expecting TestActivity$it" + ) + .that(isInLaunchedApp(launcher)) + .isTrue() + } + } + } + + private fun assertTestAppLaunched(index: Int) { + assertWithMessage("TestActivity$index not opened in Desktop") + .that( + mDevice.wait( + Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")), + TestUtil.DEFAULT_UI_TIMEOUT, + ) + ) + .isTrue() + } + + companion object { + const val TEST_ACTIVITY_1 = 2 + const val TEST_ACTIVITY_2 = 3 + const val TEST_ACTIVITY_EXTRA = 4 + val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2) + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java index df73e0913e..a16811efaf 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java @@ -15,16 +15,12 @@ */ package com.android.quickstep; -import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT; - import android.graphics.Rect; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; -import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; -import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; import org.junit.Assert; import org.junit.Test; @@ -35,8 +31,6 @@ import org.junit.runner.RunWith; public class TaplTestsPersistentTaskbar extends AbstractTaplTestsTaskbar { @Test - @TaskbarModeSwitch(mode = PERSISTENT) - @PortraitLandscape @NavigationModeSwitch public void testTaskbarFillsWidth() { // Width check is performed inside TAPL whenever getTaskbar() is called. diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java index 7877e8ac22..f8a123c918 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -16,12 +16,11 @@ package com.android.quickstep; -import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; -import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; @@ -29,14 +28,16 @@ import static org.junit.Assume.assumeTrue; import android.content.Intent; import android.content.res.Configuration; +import android.platform.test.annotations.EnableFlags; +import androidx.annotation.NonNull; import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import androidx.test.uiautomator.By; import androidx.test.uiautomator.Until; -import com.android.launcher3.Launcher; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherState; import com.android.launcher3.tapl.BaseOverview; import com.android.launcher3.tapl.LaunchedAppState; @@ -46,20 +47,21 @@ import com.android.launcher3.tapl.OverviewActions; import com.android.launcher3.tapl.OverviewTask; import com.android.launcher3.tapl.SelectModeButtons; import com.android.launcher3.tapl.Workspace; -import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.launcher3.util.TestUtil; import com.android.launcher3.util.Wait; -import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord; -import com.android.launcher3.util.rule.TestStabilityRule; +import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; import com.android.quickstep.views.RecentsView; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Comparator; +import java.util.Optional; + @LargeTest @RunWith(AndroidJUnit4.class) public class TaplTestsQuickstep extends AbstractQuickStepTest { @@ -72,18 +74,14 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { @Before public void setUp() throws Exception { super.setUp(); - executeOnLauncher(launcher -> { - RecentsView recentsView = launcher.getOverviewPanel(); - recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(true); - }); + executeOnOverview(recentsView -> + recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(true)); } @After public void tearDown() { - executeOnLauncherInTearDown(launcher -> { - RecentsView recentsView = launcher.getOverviewPanel(); - recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false); - }); + executeOnOverview(/* forTearDown= */ true, recentsView -> + recentsView.getPagedViewOrientedState().forceAllowRotationForTesting(false)); } public static void startTestApps() throws Exception { @@ -92,14 +90,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestActivity(2); } - private void startTestAppsWithCheck() throws Exception { - startTestApps(); - executeOnLauncher(launcher -> assertTrue( - "Launcher activity is the top activity; expecting another activity to be the top " - + "one", - isInLaunchedApp(launcher))); - } - @Test @NavigationModeSwitch @PortraitLandscape @@ -116,28 +106,28 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); // mLauncher.pressHome() also tests an important case of pressing home while in background. Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher( - launcher -> assertTrue("Don't have at least 3 tasks", getTaskCount(launcher) >= 3)); + assertIsInState( + "Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW); + executeOnOverview(recentsView -> assertTrue("Don't have at least 3 tasks", + recentsView.getTaskViewCount() >= 3)); // Test flinging forward and backward. - executeOnLauncher(launcher -> assertEquals("Current task in Overview is not 0", - 0, getCurrentOverviewPage(launcher))); + executeOnOverview(recentsView -> assertEquals("Current task in Overview is not first", + recentsView.indexOfChild(recentsView.getFirstTaskView()), + recentsView.getCurrentPage())); overview.flingForward(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - final Integer currentTaskAfterFlingForward = getFromLauncher( - launcher -> getCurrentOverviewPage(launcher)); - executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0", - currentTaskAfterFlingForward > 0)); + assertIsInState("Launcher internal state is not Overview", LauncherState.OVERVIEW); + final Integer currentTaskAfterFlingForward = + getFromOverview(RecentsView::getCurrentPage); + executeOnOverview(recentsView -> assertTrue("Current task in Overview is still 0", + currentTaskAfterFlingForward > recentsView.indexOfChild( + recentsView.getFirstTaskView()))); overview.flingBackward(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher(launcher -> assertTrue("Flinging back in Overview did nothing", - getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward)); + assertIsInState("Launcher internal state is not Overview", LauncherState.OVERVIEW); + executeOnOverview(recentsView -> assertTrue("Flinging back in Overview did nothing", + recentsView.getCurrentPage() < currentTaskAfterFlingForward)); // Test opening a task. OverviewTask task = mLauncher.goHome().switchToOverview().getCurrentTask(); @@ -145,31 +135,26 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { assertNotNull("OverviewTask.open returned null", task.open()); assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject( By.pkg(getAppPackageName()).text("TestActivity2")), - DEFAULT_UI_TIMEOUT)); - executeOnLauncher(launcher -> assertTrue( - "Launcher activity is the top activity; expecting another activity to be the top " - + "one", - isInLaunchedApp(launcher))); + TestUtil.DEFAULT_UI_TIMEOUT)); + expectLaunchedAppState(); // Test dismissing a task. overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); - final Integer numTasks = getFromLauncher(launcher -> getTaskCount(launcher)); + assertIsInState("Launcher internal state didn't switch to Overview", + LauncherState.OVERVIEW); + final Integer numTasks = getFromOverview(RecentsView::getTaskViewCount); task = overview.getCurrentTask(); assertNotNull("overview.getCurrentTask() returned null (2)", task); task.dismiss(); - executeOnLauncher( - launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview", - numTasks - 1, getTaskCount(launcher))); + executeOnOverview(recentsView -> assertEquals( + "Dismissing a task didn't remove 1 task from Overview", + numTasks - 1, recentsView.getTaskViewCount())); // Test dismissing all tasks. mLauncher.goHome().switchToOverview().dismissAllTasks(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); - executeOnLauncher( - launcher -> assertEquals("Still have tasks after dismissing all", - 0, getTaskCount(launcher))); + assertIsInState("Launcher internal state is not Home", LauncherState.NORMAL); + executeOnOverview(recentsView -> assertEquals("Still have tasks after dismissing all", + 0, recentsView.getTaskViewCount())); } /** @@ -191,12 +176,10 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { public void testDismissOverviewWithEscKey() throws Exception { startTestAppsWithCheck(); final Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state is not Overview", LauncherState.OVERVIEW); overview.dismissByEscKey(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state is not Home", LauncherState.NORMAL); } @Test @@ -212,16 +195,15 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { selectModeButtons = overview.getOverviewActions().clickSelect(); } - assertTrue("Launcher internal state is not Overview Modal Task", - isInState(() -> LauncherState.OVERVIEW_MODAL_TASK)); + assertIsInState( + "Launcher internal state is not Overview Modal Task", + LauncherState.OVERVIEW_MODAL_TASK); selectModeButtons.dismissByEscKey(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state is not Overview", LauncherState.OVERVIEW); overview.dismissByEscKey(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state is not Home", LauncherState.NORMAL); } @Test @@ -229,11 +211,11 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); startAppFast(CALCULATOR_APP_PACKAGE); // Ensure Calculator is last opened app. Workspace home = mLauncher.goHome(); - assertTrue("Launcher state is not Home", isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher state is not Home", LauncherState.NORMAL); Overview overview = home.openOverviewFromActionPlusTabKeyboardShortcut(); - assertTrue("Launcher state is not Overview", isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher state is not Overview", LauncherState.OVERVIEW); overview.launchFocusedTaskByEnterKey(CALCULATOR_APP_PACKAGE); // Assert app is focused. } @@ -242,33 +224,14 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); startAppFast(CALCULATOR_APP_PACKAGE); // Ensure Calculator is last opened app. Workspace home = mLauncher.goHome(); - assertTrue("Launcher state is not Home", isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher state is not Home", LauncherState.NORMAL); Overview overview = home.openOverviewFromRecentsKeyboardShortcut(); - assertTrue("Launcher state is not Overview", isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher state is not Overview", LauncherState.OVERVIEW); overview.launchFocusedTaskByEnterKey(CALCULATOR_APP_PACKAGE); // Assert app is focused. } - private int getCurrentOverviewPage(Launcher launcher) { - return launcher.getOverviewPanel().getCurrentPage(); - } - - private int getTaskCount(Launcher launcher) { - return launcher.getOverviewPanel().getTaskViewCount(); - } - - private int getTopRowTaskCountForTablet(Launcher launcher) { - return launcher.getOverviewPanel().getTopRowTaskCountForTablet(); - } - - private int getBottomRowTaskCountForTablet(Launcher launcher) { - return launcher.getOverviewPanel().getBottomRowTaskCountForTablet(); - } - - // Staging; will be promoted to presubmit if stable - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) - @Test @NavigationModeSwitch @PortraitLandscape @@ -276,8 +239,8 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); assertNotNull("Workspace.switchToOverview() returned null", mLauncher.goHome().switchToOverview()); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState( + "Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW); } @Test @@ -293,9 +256,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { } } - // Staging; will be promoted to presubmit if stable - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) - @Test @NavigationModeSwitch @PortraitLandscape @@ -305,28 +265,13 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { assertNotNull("Background.switchToOverview() returned null", launchedAppState.switchToOverview()); - assertTrue("Launcher internal state didn't switch to Overview", - isInState(() -> LauncherState.OVERVIEW)); - } - - private void quickSwitchToPreviousAppAndAssert(boolean toRight) { - final LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); - if (toRight) { - launchedAppState.quickSwitchToPreviousApp(); - } else { - launchedAppState.quickSwitchToPreviousAppSwipeLeft(); - } - - // While enable shell transition, Launcher can be resumed due to transient launch. - waitForLauncherCondition("Launcher shouldn't stay in resume forever", - this::isInLaunchedApp, 3000 /* timeout */); + assertIsInState( + "Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW); } @Test @NavigationModeSwitch @PortraitLandscape - @ScreenRecord // b/313464374 - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406 public void testQuickSwitchFromApp() throws Exception { startTestActivity(2); startTestActivity(3); @@ -393,11 +338,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { } } - private boolean isHardwareKeyboard() { - return Configuration.KEYBOARD_QWERTY - == mTargetContext.getResources().getConfiguration().keyboard; - } - @Test @NavigationModeSwitch @PortraitLandscape @@ -418,81 +358,11 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { // Debug if we need to goHome to prevent wrong previous state b/315525621 mLauncher.goHome(); mLauncher.getWorkspace().switchToAllApps().pressBackToWorkspace(); - waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); + waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL); startAppFast(CALCULATOR_APP_PACKAGE); mLauncher.getLaunchedAppState().pressBackToWorkspace(); - waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); - } - - @Test - @PortraitLandscape - @TaskbarModeSwitch() - @Ignore("b/315376057") - public void testOverviewForTablet() throws Exception { - assumeTrue(mLauncher.isTablet()); - - for (int i = 2; i <= 14; i++) { - startTestActivity(i); - } - - Overview overview = mLauncher.goHome().switchToOverview(); - executeOnLauncher( - launcher -> assertTrue("Don't have at least 13 tasks", - getTaskCount(launcher) >= 13)); - - // Test scroll the first task off screen - overview.scrollCurrentTaskOffScreen(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0", - getCurrentOverviewPage(launcher) > 0)); - - // Test opening the task. - overview.getCurrentTask().open(); - assertTrue("Test activity didn't open from Overview", - mDevice.wait(Until.hasObject(By.pkg(getAppPackageName()).text( - mLauncher.isGridOnlyOverviewEnabled() ? "TestActivity12" - : "TestActivity13")), - DEFAULT_UI_TIMEOUT)); - - // Scroll the task offscreen as it is now first - overview = mLauncher.goHome().switchToOverview(); - overview.scrollCurrentTaskOffScreen(); - assertTrue("Launcher internal state is not Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0", - getCurrentOverviewPage(launcher) > 0)); - - // Test dismissing the later task. - final Integer numTasks = getFromLauncher(this::getTaskCount); - overview.getCurrentTask().dismiss(); - executeOnLauncher( - launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview", - numTasks - 1, getTaskCount(launcher))); - executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after dismissal", - (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet( - launcher)) <= 1))); - - // TODO(b/308841019): Re-enable after fixing Overview jank when dismiss -// // Test dismissing more tasks. -// assertTrue("Launcher internal state didn't remain in Overview", -// isInState(() -> LauncherState.OVERVIEW)); -// overview.getCurrentTask().dismiss(); -// assertTrue("Launcher internal state didn't remain in Overview", -// isInState(() -> LauncherState.OVERVIEW)); -// overview.getCurrentTask().dismiss(); -// executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple dismissals", -// (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet( -// launcher)) <= 1))); - - // Test dismissing all tasks. - mLauncher.goHome().switchToOverview().dismissAllTasks(); - assertTrue("Launcher internal state is not Home", - isInState(() -> LauncherState.NORMAL)); - executeOnLauncher( - launcher -> assertEquals("Still have tasks after dismissing all", - 0, getTaskCount(launcher))); + waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL); } @Test @@ -501,22 +371,18 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher( - launcher -> assertTrue("Should have at least 3 tasks", - getTaskCount(launcher) >= 3)); + assertIsInState("Launcher internal state should be Overview", LauncherState.OVERVIEW); + executeOnOverview(recentsView -> assertTrue("Should have at least 3 tasks", + recentsView.getTaskViewCount() >= 3)); // It should not dismiss overview when tapping between tasks overview.touchBetweenTasks(); overview = mLauncher.getOverview(); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state should be Overview", LauncherState.OVERVIEW); // Dismiss when tapping to the right of the focused task overview.touchOutsideFirstTask(); - assertTrue("Launcher internal state should be Home", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state should be Home", LauncherState.NORMAL); } @Test @@ -528,34 +394,28 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { startTestAppsWithCheck(); Overview overview = mLauncher.goHome().switchToOverview(); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); - executeOnLauncher( - launcher -> assertTrue("Should have at least 3 tasks", - getTaskCount(launcher) >= 3)); + assertIsInState("Launcher internal state should be Overview", LauncherState.OVERVIEW); + executeOnOverview(recentsView -> assertTrue("Should have at least 3 tasks", + recentsView.getTaskViewCount() >= 3)); if (mLauncher.isTransientTaskbar()) { // On transient taskbar, it should dismiss when tapping outside taskbar bounds. overview.touchTaskbarBottomCorner(/* tapRight= */ false); - assertTrue("Launcher internal state should be Normal", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state should be Normal", LauncherState.NORMAL); overview = mLauncher.getWorkspace().switchToOverview(); // On transient taskbar, it should dismiss when tapping outside taskbar bounds. overview.touchTaskbarBottomCorner(/* tapRight= */ true); - assertTrue("Launcher internal state should be Normal", - isInState(() -> LauncherState.NORMAL)); + assertIsInState("Launcher internal state should be Normal", LauncherState.NORMAL); } else { // On persistent taskbar, it should not dismiss when tapping the taskbar overview.touchTaskbarBottomCorner(/* tapRight= */ false); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state should be Overview", LauncherState.OVERVIEW); // On persistent taskbar, it should not dismiss when tapping the taskbar overview.touchTaskbarBottomCorner(/* tapRight= */ true); - assertTrue("Launcher internal state should be Overview", - isInState(() -> LauncherState.OVERVIEW)); + assertIsInState("Launcher internal state should be Overview", LauncherState.OVERVIEW); } } @@ -568,7 +428,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { mLauncher.getDevice().setOrientationLeft(); startTestActivity(7); Wait.atMost("Device should not be in natural orientation", - () -> !mDevice.isNaturalOrientation(), DEFAULT_UI_TIMEOUT, mLauncher); + () -> !mDevice.isNaturalOrientation(), mLauncher); mLauncher.goHome(); } finally { mLauncher.setExpectedRotationCheckEnabled(true); @@ -581,20 +441,211 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { public void testExcludeFromRecents() throws Exception { startExcludeFromRecentsTestActivity(); OverviewTask currentTask = getAndAssertLaunchedApp().switchToOverview().getCurrentTask(); - // TODO(b/326565120): the expected content description shouldn't be null but for now there - // is a bug that causes it to sometimes be for excludeForRecents tasks. assertTrue("Can't find ExcludeFromRecentsTestActivity after entering Overview from it", - currentTask.containsContentDescription("ExcludeFromRecents") - || currentTask.containsContentDescription(null)); + currentTask.containsContentDescription("ExcludeFromRecents")); // Going home should clear out the excludeFromRecents task. BaseOverview overview = mLauncher.goHome().switchToOverview(); if (overview.hasTasks()) { currentTask = overview.getCurrentTask(); assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home", - currentTask.containsContentDescription("ExcludeFromRecents") - || currentTask.containsContentDescription(null)); + currentTask.containsContentDescription("ExcludeFromRecents")); } else { // Presumably the test started with 0 tasks and remains that way after going home. } } + + @Test + @PortraitLandscape + public void testDismissCancel() throws Exception { + startTestAppsWithCheck(); + Overview overview = mLauncher.goHome().switchToOverview(); + assertIsInState("Launcher internal state didn't switch to Overview", + LauncherState.OVERVIEW); + final Integer numTasks = getFromOverview(RecentsView::getTaskViewCount); + OverviewTask task = overview.getCurrentTask(); + assertNotNull("overview.getCurrentTask() returned null (2)", task); + + task.dismissCancel(); + + executeOnOverview(recentsView -> assertEquals( + "Canceling dismissing a task removed a task from Overview", + numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount())); + } + + @Test + @PortraitLandscape + @EnableFlags(value = Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW) + public void testDismissBottomRow() throws Exception { + assumeTrue(mLauncher.isTablet()); + clearAllRecentTasks(); + startTestAppsWithCheck(); + + Overview overview = mLauncher.goHome().switchToOverview(); + assertIsInState("Launcher internal state didn't switch to Overview", + LauncherState.OVERVIEW); + final Integer numTasks = getFromOverview(RecentsView::getTaskViewCount); + Optional bottomTask = overview.getCurrentTasksForTablet().stream().max( + Comparator.comparingInt(OverviewTask::getTaskCenterY)); + assertTrue("bottomTask null", bottomTask.isPresent()); + + bottomTask.get().dismiss(); + executeOnOverview(recentsView -> assertEquals( + "Dismissing a bottomTask didn't remove 1 bottomTask from Overview", + numTasks - 1, recentsView.getTaskViewCount())); + } + + @Test + @PortraitLandscape + @EnableFlags(value = Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW) + public void testDismissLastGridRow() throws Exception { + assumeTrue(mLauncher.isTablet()); + clearAllRecentTasks(); + startTestAppsWithCheck(); + startTestActivity(3); + startTestActivity(4); + executeOnOverview(recentsView -> assertNotEquals( + "Grid overview should have unequal row counts", + recentsView.getTopRowTaskCountForTablet(), + recentsView.getBottomRowTaskCountForTablet())); + Overview overview = mLauncher.goHome().switchToOverview(); + assertIsInState("Launcher internal state didn't switch to Overview", + LauncherState.OVERVIEW); + + overview.flingForwardUntilClearAllVisible(); + assertTrue("Clear All not visible.", overview.isClearAllVisible()); + final Integer numTasks = getFromOverview(RecentsView::getTaskViewCount); + Optional lastGridTask = overview.getCurrentTasksForTablet().stream().min( + Comparator.comparingInt(OverviewTask::getTaskCenterX)); + assertTrue("lastGridTask null.", lastGridTask.isPresent()); + + lastGridTask.get().dismiss(); + executeOnOverview(recentsView -> { + assertEquals( + "Dismissing a lastGridTask didn't remove 1 lastGridTask from Overview", + numTasks - 1, recentsView.getTaskViewCount()); + assertEquals( + "Grid overview should have equal row counts.", + recentsView.getTopRowTaskCountForTablet(), + recentsView.getBottomRowTaskCountForTablet()); + }); + assertTrue("Clear All not visible.", overview.isClearAllVisible()); + } + + @Test + @PortraitLandscape + @EnableFlags(value = Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW) + // When dismissing multiple apps, the apps off screen should "re-balance" i.e. re-arrange + // themselves evenly across both top and bottom rows. + public void gridRebalancesOffScreenAfterDismissingMultipleApps() throws Exception { + assumeTrue(mLauncher.isTablet()); + clearAllRecentTasks(); + // Launch enough apps so some are offscreen. + for (int i = 2; i <= 12; i++) { + startTestActivity(i); + } + Overview overview = mLauncher.goHome().switchToOverview(); + executeOnOverview(recentsView -> assertTrue("11 tasks should be open", + recentsView.getTaskViewCount() >= 11)); + + // Dismiss 2 tasks from the top row. + assertIsInState( + "Launcher internal state didn't remain in Overview", LauncherState.OVERVIEW); + overview.getCurrentTask().dismiss(); + assertIsInState( + "Launcher internal state didn't remain in Overview", LauncherState.OVERVIEW); + overview.getCurrentTask().dismiss(); + + // Assert that the two row counts are no more than 1 apart, therefore were re-balanced. + executeOnOverview(recentsView -> assertTrue( + "Grid did not re-balance after multiple dismissals", + (Math.abs(recentsView.getTopRowTaskCountForTablet() + - recentsView.getBottomRowTaskCountForTablet()) <= 1))); + } + + @Test + @PortraitLandscape + @EnableFlags(value = Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW) + // When dismissing multiple apps, the apps on screen should not "re-balance" i.e. dismissing + // 2 apps from the top row, will move the top row along 2 and so it will not be balanced + // across the bottom row. + public void gridDoesNotRebalanceOnScreenAfterDismissingMultipleApps() throws Exception { + assumeTrue(mLauncher.isTablet()); + clearAllRecentTasks(); + // Launch 6 apps so 3 are in each row. + int appsInBothRowsCount = 6; + int appsInEachRowCount = appsInBothRowsCount / 2; + for (int i = 2; i <= appsInBothRowsCount + 1; i++) { + startTestActivity(i); + } + Overview overview = mLauncher.goHome().switchToOverview(); + executeOnOverview(recentsView -> { + assertEquals(appsInBothRowsCount + " tasks should be open", + appsInBothRowsCount, recentsView.getTaskViewCount()); + assertEquals("Grid should have " + appsInEachRowCount + " tasks on the top row", + appsInEachRowCount, + recentsView.getTopRowTaskCountForTablet()); + assertEquals("Grid should have " + appsInEachRowCount + " tasks on the bottom row", + appsInEachRowCount, + recentsView.getBottomRowTaskCountForTablet()); + }); + + // Dismiss 2 tasks from the top row. + assertIsInState("Launcher internal state didn't remain in Overview", + LauncherState.OVERVIEW); + overview.getCurrentTask().dismiss(); + assertIsInState("Launcher internal state didn't remain in Overview", + LauncherState.OVERVIEW); + overview.getCurrentTask().dismiss(); + + executeOnOverview(recentsView -> { + int expectedTopRowCount = appsInEachRowCount - 2; + assertEquals( + "Grid should have " + expectedTopRowCount + " tasks on the top row", + expectedTopRowCount, + recentsView.getTopRowTaskCountForTablet()); + assertEquals("Grid should have " + appsInEachRowCount + " tasks on the bottom row", + appsInEachRowCount, + recentsView.getBottomRowTaskCountForTablet()); + }); + } + + private void startTestAppsWithCheck() throws Exception { + startTestApps(); + expectLaunchedAppState(); + } + + private void quickSwitchToPreviousAppAndAssert(boolean toRight) { + final LaunchedAppState launchedAppState = getAndAssertLaunchedApp(); + if (toRight) { + launchedAppState.quickSwitchToPreviousApp(); + } else { + launchedAppState.quickSwitchToPreviousAppSwipeLeft(); + } + + // While enable shell transition, Launcher can be resumed due to transient launch. + waitForLauncherCondition("Launcher shouldn't stay in resume forever", + this::isInLaunchedApp, 3000 /* timeout */); + } + + private boolean isHardwareKeyboard() { + return Configuration.KEYBOARD_QWERTY + == mTargetContext.getResources().getConfiguration().keyboard; + } + + private void assertIsInState( + @NonNull String failureMessage, @NonNull LauncherState expectedState) { + assertTrue(failureMessage, isInState(() -> expectedState)); + } + + private void waitForState( + @NonNull String failureMessage, @NonNull LauncherState expectedState) { + waitForState(failureMessage, () -> expectedState); + } + + private void expectLaunchedAppState() { + executeOnLauncher(launcher -> assertTrue( + "Launcher activity is the top activity; expecting another activity to be the top " + + "one", + isInLaunchedApp(launcher))); + } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java index 8adf79318b..37ac4a0ed2 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java @@ -16,10 +16,6 @@ package com.android.quickstep; -import static com.android.launcher3.config.FeatureFlags.enableSplitContextually; -import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; -import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; @@ -33,8 +29,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.launcher3.tapl.Overview; import com.android.launcher3.tapl.Taskbar; import com.android.launcher3.tapl.TaskbarAppIcon; -import com.android.launcher3.util.rule.TestStabilityRule; -import com.android.wm.shell.Flags; +import com.android.quickstep.util.SplitScreenTestUtils; import org.junit.After; import org.junit.Before; @@ -72,7 +67,6 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { } @Test - @TestStabilityRule.Stability(flavors = PLATFORM_POSTSUBMIT | LOCAL) // b/295225524 public void testSplitAppFromHomeWithItself() throws Exception { // Currently only tablets have Taskbar in Overview, so test is only active on tablets assumeTrue(mLauncher.isTablet()); @@ -92,28 +86,16 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { .getSplitScreenMenuItem() .click(); - if (enableSplitContextually()) { - // We're staying in all apps, use same instance - mLauncher.getAllApps() - .getAppIcon(CALCULATOR_APP_NAME) - .launchIntoSplitScreen(); - } else { - // We're in overview, use taskbar instance - mLauncher.getLaunchedAppState() - .getTaskbar() - .getAppIcon(CALCULATOR_APP_NAME) - .launchIntoSplitScreen(); - } + // We're staying in all apps, use same instance + mLauncher.getAllApps() + .getAppIcon(CALCULATOR_APP_NAME) + .launchIntoSplitScreen(); } @Test public void testSaveAppPairMenuItemOrActionExistsOnSplitPair() { - assumeTrue("App pairs feature is currently not enabled, no test needed", - Flags.enableAppPairs()); + Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher); - createAndLaunchASplitPair(); - - Overview overview = mLauncher.goHome().switchToOverview(); if (mLauncher.isGridOnlyOverviewEnabled() || !mLauncher.isTablet()) { assertTrue("Save app pair menu item is missing", overview.getCurrentTask() @@ -124,9 +106,6 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { @Test public void testSaveAppPairMenuItemDoesNotExistOnSingleTask() throws Exception { - assumeTrue("App pairs feature is currently not enabled, no test needed", - Flags.enableAppPairs()); - startAppFast(CALCULATOR_APP_PACKAGE); assertFalse("Save app pair menu item is erroneously appearing on single task", @@ -157,24 +136,4 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest { TaskbarAppIcon firstApp = taskbar.getAppIcon(firstAppName); firstApp.launchIntoSplitScreen(); } - - private void createAndLaunchASplitPair() { - clearAllRecentTasks(); - - startTestActivity(2); - startTestActivity(3); - - if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) { - mLauncher.goHome().switchToOverview().getOverviewActions() - .clickSplit() - .getTestActivityTask(2) - .open(); - } else { - mLauncher.goHome().switchToOverview().getCurrentTask() - .tapMenu() - .tapSplitMenuItem() - .getCurrentTask() - .open(); - } - } } diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java index ec245ee0f3..0d1d2bd16c 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java @@ -23,7 +23,7 @@ import static com.android.quickstep.TaplTestsTaskbar.TaskbarMode.TRANSIENT; import androidx.test.filters.LargeTest; -import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java index 2c23f867cb..8cb1c622df 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java @@ -16,8 +16,6 @@ package com.android.quickstep; -import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; -import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON; import static org.junit.Assert.assertNotNull; @@ -31,9 +29,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType; import com.android.launcher3.tapl.Workspace; -import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; -import com.android.launcher3.util.rule.ScreenRecordRule; -import com.android.launcher3.util.rule.TestStabilityRule; +import com.android.launcher3.util.ui.PortraitLandscapeRunner.PortraitLandscape; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import org.junit.After; @@ -95,8 +91,6 @@ public class TaplTestsTrackpad extends AbstractQuickStepTest { @Test @PortraitLandscape @NavigationModeSwitch - @ScreenRecordRule.ScreenRecord // b/336606166 - @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/336606166 public void switchToOverview() throws Exception { assumeTrue(mLauncher.isTablet()); diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java index 4b20d600dc..821d14fd35 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java @@ -18,6 +18,7 @@ package com.android.quickstep; import static com.android.launcher3.Flags.enableCursorHoverStates; import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME; import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT; +import static com.android.systemui.shared.Flags.cursorHotCorner; import static org.junit.Assume.assumeTrue; @@ -41,14 +42,6 @@ public class TaplTestsTransientTaskbar extends AbstractTaplTestsTaskbar { mLauncher.getLaunchedAppState().hoverToShowTaskbarUnstashHint(); } - @Test - @TaskbarModeSwitch(mode = TRANSIENT) - public void testUnstashTaskbarOnScreenBottomEdgeHover() { - assumeTrue(enableCursorHoverStates()); - getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); - mLauncher.getLaunchedAppState().hoverScreenBottomEdgeToUnstashTaskbar(); - } - @Test @TaskbarModeSwitch(mode = TRANSIENT) public void testHoverBelowHintedTaskbarToUnstash() { @@ -71,4 +64,31 @@ public class TaplTestsTransientTaskbar extends AbstractTaplTestsTaskbar { getTaskbar().swipeDownToStash(); mLauncher.getLaunchedAppState().swipeUpToUnstashTaskbar(); } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testUnstashTaskbarOnScreenBottomEdgeHover() { + assumeTrue(enableCursorHoverStates()); + getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); + mLauncher.getLaunchedAppState().hoverScreenBottomEdgeToUnstashTaskbar(); + mLauncher.getLaunchedAppState().assertTaskbarVisible(); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testUnstashTaskbarOnScreenBottomEdgeOutsideActionCornerHover() { + assumeTrue(cursorHotCorner()); + getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); + mLauncher.getLaunchedAppState().hoverScreenBottomEdgeOutsideActionCornerToUnstashTaskbar(); + mLauncher.getLaunchedAppState().assertTaskbarVisible(); + } + + @Test + @TaskbarModeSwitch(mode = TRANSIENT) + public void testNotShowTaskbarOnActionCornerPaddingHover() { + assumeTrue(cursorHotCorner()); + getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE); + mLauncher.getLaunchedAppState().hoverScreenBottomCornerToTryUnstashTaskbar(); + mLauncher.getLaunchedAppState().assertTaskbarHidden(); + } } diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java index 512557bf3a..6b87f94cf8 100644 --- a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java +++ b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java @@ -16,6 +16,9 @@ package com.android.quickstep; +import static com.android.quickstep.TaskViewTestDIHelpers.initializeRecentsDependencies; +import static com.android.quickstep.TaskViewTestDIHelpers.mockRecentsModel; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -35,11 +38,15 @@ import android.util.DisplayMetrics; import android.view.MotionEvent; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.launcher3.uioverrides.QuickstepLauncher; +import com.android.launcher3.util.SandboxContext; +import com.android.quickstep.recents.di.RecentsDependencies; import com.android.quickstep.util.BorderAnimator; import com.android.quickstep.views.TaskView; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -48,6 +55,8 @@ import org.mockito.MockitoAnnotations; @SmallTest public class TaskViewTest { + private final SandboxContext mApplicationContext = + new SandboxContext(InstrumentationRegistry.getInstrumentation().getTargetContext()); @Mock private QuickstepLauncher mContext; @Mock @@ -69,10 +78,19 @@ public class TaskViewTest { when(mContext.getApplicationInfo()).thenReturn(mock(ApplicationInfo.class)); when(mContext.obtainStyledAttributes(any(), any(), anyInt(), anyInt())).thenReturn( mock(TypedArray.class)); + when(mContext.getApplicationContext()).thenReturn(mApplicationContext); + mApplicationContext.initDaggerComponent( + DaggerTaskViewTestComponent.builder().bindRecentsModel(mockRecentsModel())); + initializeRecentsDependencies(mContext); mTaskView = new TaskView(mContext, null, 0, 0, mFocusAnimator, mHoverAnimator); } + @After + public void tearDown() { + RecentsDependencies.destroy(mContext); + } + @Test public void notShowBorderOnBorderDisabled() { presetBorderStatus(/* enabled= */ true); @@ -87,18 +105,6 @@ public class TaskViewTest { true); } - @Test - public void showBorderOnHoverEvent() { - mTaskView.setBorderEnabled(/* enabled= */ true); - MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0); - mTaskView.onHoverEvent(MotionEvent.obtain(event)); - verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */ - true); - mTaskView.onFocusChanged(true, 0, new Rect()); - verify(mFocusAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */ - true); - } - @Test public void showBorderOnBorderEnabled() { presetBorderStatus(/* enabled= */ false); diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTestDIHelpers.kt b/quickstep/tests/src/com/android/quickstep/TaskViewTestDIHelpers.kt new file mode 100644 index 0000000000..283010e63b --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaskViewTestDIHelpers.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.content.Context +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.AllModulesMinusWMProxy +import com.android.launcher3.util.TestDispatcherProvider +import com.android.launcher3.util.coroutines.DispatcherProvider +import com.android.quickstep.recents.data.AppTimersRepository +import com.android.quickstep.recents.data.FakeAppTimersRepository +import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository +import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository +import com.android.quickstep.recents.data.RecentsDeviceProfileRepository +import com.android.quickstep.recents.data.RecentsRotationStateRepository +import com.android.quickstep.recents.di.RecentsDependencies.Companion.maybeInitialize +import dagger.BindsInstance +import dagger.Component +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@LauncherAppSingleton +@Component(modules = [AllModulesMinusWMProxy::class]) +interface TaskViewTestComponent : LauncherAppComponent { + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + @BindsInstance fun bindRecentsModel(recentsModel: RecentsModel): Builder + + override fun build(): TaskViewTestComponent + } +} + +object TaskViewTestDIHelpers { + /** [context] is used as if it is RecentsView's context. */ + @OptIn(ExperimentalCoroutinesApi::class) + @JvmStatic + fun initializeRecentsDependencies(context: Context) { + val dp: DispatcherProvider = TestDispatcherProvider(UnconfinedTestDispatcher()) + val recentsDependencies = maybeInitialize(context, dp) + recentsDependencies.createRecentsViewScope(context) + recentsDependencies + .getScope(context)[RecentsRotationStateRepository::class.java.simpleName] = + FakeRecentsRotationStateRepository() + recentsDependencies + .getScope(context)[RecentsDeviceProfileRepository::class.java.simpleName] = + FakeRecentsDeviceProfileRepository() + recentsDependencies.getScope(context)[AppTimersRepository::class.java.simpleName] = + FakeAppTimersRepository() + } + + @JvmStatic + fun mockRecentsModel(): RecentsModel { + val recentsModel: RecentsModel = mock() + val taskThumbnailCache: TaskThumbnailCache = mock() + whenever(taskThumbnailCache.highResLoadingState).thenReturn(HighResLoadingState()) + whenever(recentsModel.thumbnailCache).thenReturn(taskThumbnailCache) + whenever(recentsModel.iconCache).thenReturn(mock()) + return recentsModel + } +} diff --git a/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java index 84ceb332d9..87cda25c3c 100644 --- a/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java +++ b/quickstep/tests/src/com/android/quickstep/TaskbarModeSwitchRule.java @@ -27,9 +27,9 @@ import android.util.Log; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.TestHelpers; -import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.rule.FailureWatcher; +import com.android.launcher3.util.ui.AbstractLauncherUiTest; import org.junit.rules.TestRule; import org.junit.runner.Description; diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt new file mode 100644 index 0000000000..daa77d2a31 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.desktop + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.util.DisplayMetrics +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import androidx.core.util.Supplier +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.android.app.animation.Interpolators +import com.android.internal.jank.Cuj +import com.android.launcher3.desktop.DesktopAppLaunchAnimatorHelper +import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class DesktopAppLaunchAnimatorHelperTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val context = mock() + private val resources = mock() + private val transaction = mock() + private val transactionSupplier = mock>() + + private lateinit var helper: DesktopAppLaunchAnimatorHelper + + @Before + fun setUp() { + helper = + DesktopAppLaunchAnimatorHelper( + context = context, + launchType = AppLaunchType.LAUNCH, + cujType = Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT, + transactionSupplier = transactionSupplier, + ) + whenever(transactionSupplier.get()).thenReturn(transaction) + whenever(transaction.setCrop(any(), any())).thenReturn(transaction) + whenever(transaction.setCornerRadius(any(), any())).thenReturn(transaction) + whenever(transaction.setScale(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setAlpha(any(), any())).thenReturn(transaction) + whenever(transaction.setFrameTimeline(any())).thenReturn(transaction) + + whenever(context.resources).thenReturn(resources) + whenever(resources.displayMetrics).thenReturn(DisplayMetrics()) + whenever(context.mainThreadHandler).thenReturn(MAIN_EXECUTOR.handler) + } + + @Test + fun launchTransition_returnsLaunchAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(1) + assertLaunchAnimator(actual[0]) + } + + @Test + fun launchTransition_callsAnimationEndListener() = runOnUiThread { + val finishCallback = mock>() + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE)) + + val animators = helper.createAnimators(transitionInfo, finishCallback = finishCallback) + + animators.forEach { animator -> + animator.start() + animator.end() + verify(finishCallback).invoke(animator) + } + } + + @Test + fun noLaunchTransition_returnsEmptyAnimatorsList() = runOnUiThread { + val pipChange = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_PIP + taskInfo = TASK_INFO_FREEFORM + } + val transitionInfo = createTransitionInfo(listOf(pipChange)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(0) + } + + @Test + fun minimizeTransition_returnsLaunchAndMinimizeAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, MINIMIZE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(2) + assertLaunchAnimator(actual[0]) + assertMinimizeAnimator(actual[1]) + } + + @Test + fun minimizeTransition_callsAnimationEndListener() = runOnUiThread { + val finishCallback = mock>() + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, MINIMIZE_CHANGE)) + + val animators = helper.createAnimators(transitionInfo, finishCallback = finishCallback) + + animators.forEach { animator -> + animator.start() + animator.end() + verify(finishCallback).invoke(animator) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagEnabled_returnsLaunchAndCloseAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(2) + assertTrampolineLaunchAnimator(actual[0]) + assertCloseAnimator(actual[1]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagEnabled_callsAnimationEndListener() = runOnUiThread { + val finishCallback = mock>() + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, CLOSE_CHANGE)) + + val animators = helper.createAnimators(transitionInfo, finishCallback = finishCallback) + + animators.forEach { animator -> + animator.start() + animator.end() + verify(finishCallback).invoke(animator) + } + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagDisabled_returnsLaunchAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo(listOf(OPEN_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(1) + assertLaunchAnimator(actual[0]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagEnabled_hitDesktopWindowLimit_returnsLaunchMinimizeCloseAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo( + listOf(OPEN_CHANGE, MINIMIZE_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(3) + assertTrampolineLaunchAnimator(actual[0]) + assertMinimizeAnimator(actual[1]) + assertCloseAnimator(actual[2]) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX) + fun trampolineTransition_flagDisabled_hitDesktopWindowLimit_returnsLaunchMinimizeAnimator() = runOnUiThread { + val transitionInfo = createTransitionInfo( + listOf(OPEN_CHANGE, MINIMIZE_CHANGE, CLOSE_CHANGE)) + + val actual = helper.createAnimators(transitionInfo, finishCallback = {}) + + assertThat(actual).hasSize(2) + assertLaunchAnimator(actual[0]) + assertMinimizeAnimator(actual[1]) + } + + private fun assertLaunchAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(AnimatorSet::class.java) + assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2) + assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[0].interpolator) + .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.interpolator) + assertThat(animator.childAnimations[0].duration) + .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.durationMs) + assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[1].interpolator).isEqualTo(Interpolators.LINEAR) + assertThat(animator.childAnimations[1].duration) + .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs) + } + + private fun assertTrampolineLaunchAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(AnimatorSet::class.java) + assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(1) + assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[0].interpolator).isEqualTo(Interpolators.LINEAR) + assertThat(animator.childAnimations[0].duration) + .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs) + } + + private fun assertMinimizeAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(AnimatorSet::class.java) + assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2) + assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[0].interpolator) + .isInstanceOf(Interpolators.STANDARD_ACCELERATE::class.java) + assertThat(animator.childAnimations[0].duration).isEqualTo(200) + assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.childAnimations[1].interpolator) + .isInstanceOf(Interpolators.LINEAR::class.java) + assertThat(animator.childAnimations[1].duration).isEqualTo(100) + } + + private fun assertCloseAnimator(animator: Animator) { + assertThat(animator).isInstanceOf(ValueAnimator::class.java) + assertThat(animator.interpolator).isInstanceOf(Interpolators.LINEAR::class.java) + assertThat(animator.duration).isEqualTo(100) + } + + private fun createTransitionInfo(changes: List): TransitionInfo { + val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0) + changes.forEach { transitionInfo.addChange(it) } + return transitionInfo + } + + private companion object { + val TASK_INFO_FREEFORM = + ActivityManager.RunningTaskInfo().apply { + baseIntent = + Intent().apply { + component = ComponentName("com.example.app", "com.example.app.MainActivity") + } + configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + } + + val OPEN_CHANGE = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_OPEN + taskInfo = TASK_INFO_FREEFORM + } + + val CLOSE_CHANGE = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_CLOSE + taskInfo = TASK_INFO_FREEFORM + } + + val MINIMIZE_CHANGE = + TransitionInfo.Change(mock(), mock()).apply { + mode = WindowManager.TRANSIT_TO_BACK + taskInfo = TASK_INFO_FREEFORM + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt new file mode 100644 index 0000000000..15fa84c6b1 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt @@ -0,0 +1,60 @@ +/* + * 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.quickstep.util + +import androidx.test.uiautomator.By +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.launcher3.tapl.Overview +import com.android.launcher3.tapl.OverviewTask +import com.android.launcher3.util.ui.AbstractLauncherUiTest + +object SplitScreenTestUtils { + + /** Creates 2 tasks and makes a split mode pair. Also asserts the accessibility labels. */ + @JvmStatic + fun createAndLaunchASplitPairInOverview(launcher: LauncherInstrumentation): Overview { + clearAllRecentTasks(launcher) + + AbstractLauncherUiTest.startTestActivity(2) + AbstractLauncherUiTest.startTestActivity(3) + + val overView = launcher.goHome().switchToOverview() + if (launcher.isTablet && !launcher.isGridOnlyOverviewEnabled) { + overView.overviewActions.clickSplit().getTestActivityTask(2).open() + } else { + overView.currentTask.tapMenu().tapSplitMenuItem().currentTask.open() + } + + val overviewWithSplitPair = launcher.goHome().switchToOverview() + val currentTask = overviewWithSplitPair.currentTask + currentTask.containsContentDescription( + By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(), + OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT, + ) + currentTask.containsContentDescription( + By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(), + OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT, + ) + return overviewWithSplitPair + } + + private fun clearAllRecentTasks(launcher: LauncherInstrumentation) { + if (launcher.recentTasks.isNotEmpty()) { + launcher.goHome().switchToOverview().dismissAllTasks() + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt index b4f1692604..faac190bec 100644 --- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt +++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectDataHolderTest.kt @@ -22,12 +22,13 @@ import android.app.ActivityTaskManager.INVALID_TASK_ID import android.content.Context import android.content.ContextWrapper import android.content.Intent +import android.os.Process import android.os.UserHandle import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.shortcuts.ShortcutKey -import com.android.launcher3.ui.AbstractLauncherUiTest import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.launcher3.util.ui.AbstractLauncherUiTest import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_PENDINGINTENT_PENDINGINTENT import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_PENDINGINTENT_TASK import com.android.quickstep.util.SplitSelectDataHolder.Companion.SPLIT_SHORTCUT_TASK @@ -54,7 +55,7 @@ class SplitSelectDataHolderTest { private val sampleTaskInfo = RunningTaskInfo() private val sampleTaskId = 10 private val sampleTaskId2 = 11 - private val sampleUser = UserHandle(0) + private val sampleUser = UserHandle(Process.myUserHandle().identifier) private val sampleIntent = Intent() private val sampleIntent2 = Intent() private val sampleShortcut = Intent() @@ -84,7 +85,7 @@ class SplitSelectDataHolderTest { sampleTaskInfo, STAGE_POSITION_TOP_OR_LEFT, null, - null + null, ) assertTrue(splitSelectDataHolder.isSplitSelectActive()) } @@ -96,7 +97,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) assertTrue(splitSelectDataHolder.isSplitSelectActive()) } @@ -108,7 +109,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - sampleTaskId + sampleTaskId, ) assertTrue(splitSelectDataHolder.isSplitSelectActive()) } @@ -120,7 +121,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) assertTrue(splitSelectDataHolder.isSplitSelectActive()) } @@ -132,7 +133,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2) assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed()) @@ -144,7 +145,7 @@ class SplitSelectDataHolderTest { sampleTaskInfo, STAGE_POSITION_TOP_OR_LEFT, null, - null + null, ) splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2) assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed()) @@ -157,7 +158,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser, sampleItemInfo2) assertTrue(splitSelectDataHolder.isBothSplitAppsConfirmed()) @@ -169,7 +170,7 @@ class SplitSelectDataHolderTest { sampleTaskInfo, STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, - null + null, ) splitSelectDataHolder.setSecondTask(sampleTaskId2, sampleItemInfo2) val launchData = splitSelectDataHolder.getSplitLaunchData() @@ -193,7 +194,7 @@ class SplitSelectDataHolderTest { sampleTaskInfo, STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, - null + null, ) splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2) val launchData = splitSelectDataHolder.getSplitLaunchData() @@ -217,7 +218,7 @@ class SplitSelectDataHolderTest { sampleTaskInfo, STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, - null + null, ) splitSelectDataHolder.setSecondTask(sampleShortcut, sampleUser, sampleItemInfo2) val launchData = splitSelectDataHolder.getSplitLaunchData() @@ -242,7 +243,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2) val launchData = splitSelectDataHolder.getSplitLaunchData() @@ -267,7 +268,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) splitSelectDataHolder.setSecondTask(sampleTaskId, sampleItemInfo2) val launchData = splitSelectDataHolder.getSplitLaunchData() @@ -292,7 +293,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) splitSelectDataHolder.setSecondTask(sampleIntent2, sampleUser, sampleItemInfo2) val launchData = splitSelectDataHolder.getSplitLaunchData() @@ -316,7 +317,7 @@ class SplitSelectDataHolderTest { sampleTaskInfo, STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, - null + null, ) val launchData = splitSelectDataHolder.getFullscreenLaunchData() @@ -340,7 +341,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) val launchData = splitSelectDataHolder.getFullscreenLaunchData() @@ -364,7 +365,7 @@ class SplitSelectDataHolderTest { STAGE_POSITION_TOP_OR_LEFT, sampleItemInfo, null, - INVALID_TASK_ID + INVALID_TASK_ID, ) val launchData = splitSelectDataHolder.getFullscreenLaunchData() @@ -387,7 +388,7 @@ class SplitSelectDataHolderTest { sampleTaskInfo, STAGE_POSITION_TOP_OR_LEFT, null, - null + null, ) splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2) splitSelectDataHolder.resetState() @@ -397,11 +398,11 @@ class SplitSelectDataHolderTest { @Test fun clearState_intent() { splitSelectDataHolder.setInitialTaskSelect( - sampleIntent, - STAGE_POSITION_TOP_OR_LEFT, - sampleItemInfo, - null, - INVALID_TASK_ID + sampleIntent, + STAGE_POSITION_TOP_OR_LEFT, + sampleItemInfo, + null, + INVALID_TASK_ID, ) splitSelectDataHolder.setSecondTask(sampleIntent, sampleUser, sampleItemInfo2) splitSelectDataHolder.resetState() diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml deleted file mode 100644 index 3d7ad2bd60..0000000000 --- a/res/anim-v33/shared_x_axis_activity_close_enter.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml deleted file mode 100644 index fb63602d4e..0000000000 --- a/res/anim-v33/shared_x_axis_activity_close_exit.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml deleted file mode 100644 index cba74ba0ec..0000000000 --- a/res/anim-v33/shared_x_axis_activity_open_enter.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml deleted file mode 100644 index 22e878d7f1..0000000000 --- a/res/anim-v33/shared_x_axis_activity_open_exit.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_highest.xml b/res/color-night-v31/material_color_surface_container_highest.xml deleted file mode 100644 index e54f95380e..0000000000 --- a/res/color-night-v31/material_color_surface_container_highest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_low.xml b/res/color-night-v31/material_color_surface_container_low.xml deleted file mode 100644 index 40f0d4c9de..0000000000 --- a/res/color-night-v31/material_color_surface_container_low.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_lowest.xml b/res/color-night-v31/material_color_surface_container_lowest.xml index 24f559b494..4396f6d013 100644 --- a/res/color-night-v31/material_color_surface_container_lowest.xml +++ b/res/color-night-v31/material_color_surface_container_lowest.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_inverse.xml b/res/color-night-v31/material_color_surface_inverse.xml deleted file mode 100644 index ac63072a9e..0000000000 --- a/res/color-night-v31/material_color_surface_inverse.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml index 6909f81587..e62ed9c8a9 100644 --- a/res/color-night-v31/popup_shade_first.xml +++ b/res/color-night-v31/popup_shade_first.xml @@ -12,7 +12,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + diff --git a/res/color-v31/material_color_surface_container_high.xml b/res/color-v31/material_color_surface_container_high.xml deleted file mode 100644 index a996d51eba..0000000000 --- a/res/color-v31/material_color_surface_container_high.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container_highest.xml b/res/color-v31/material_color_surface_container_highest.xml deleted file mode 100644 index e7a535af53..0000000000 --- a/res/color-v31/material_color_surface_container_highest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container_low.xml b/res/color-v31/material_color_surface_container_low.xml deleted file mode 100644 index b8fe01e484..0000000000 --- a/res/color-v31/material_color_surface_container_low.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_container_lowest.xml b/res/color-v31/material_color_surface_container_lowest.xml index 25e8666862..f726aea081 100644 --- a/res/color-v31/material_color_surface_container_lowest.xml +++ b/res/color-v31/material_color_surface_container_lowest.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/res/color-v31/material_color_surface_dim.xml b/res/color-v31/material_color_surface_dim.xml deleted file mode 100644 index e2d226fa89..0000000000 --- a/res/color-v31/material_color_surface_dim.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color-v31/material_color_surface_variant.xml b/res/color-v31/material_color_surface_variant.xml deleted file mode 100644 index e2d226fa89..0000000000 --- a/res/color-v31/material_color_surface_variant.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/color/popup_color_background.xml b/res/color-v31/nudge_button_color.xml similarity index 84% rename from res/color/popup_color_background.xml rename to res/color-v31/nudge_button_color.xml index e87e77231c..2f8b1d5708 100644 --- a/res/color/popup_color_background.xml +++ b/res/color-v31/nudge_button_color.xml @@ -1,5 +1,5 @@ - - - + + diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml index 4b50cba3c2..9a71caeb51 100644 --- a/res/color-v31/popup_shade_first.xml +++ b/res/color-v31/popup_shade_first.xml @@ -13,7 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml index 3cca9c91b7..aa6c618ef7 100644 --- a/res/color/overview_button.xml +++ b/res/color/overview_button.xml @@ -1,12 +1,11 @@ - + \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_container_high.xml b/res/color/overview_scrim_foreground_primary.xml similarity index 77% rename from res/color-night-v31/material_color_surface_container_high.xml rename to res/color/overview_scrim_foreground_primary.xml index edd36fcd23..68be32d4fa 100644 --- a/res/color-night-v31/material_color_surface_container_high.xml +++ b/res/color/overview_scrim_foreground_primary.xml @@ -1,6 +1,5 @@ - - - - \ No newline at end of file + + diff --git a/res/color-night-v31/material_color_surface_bright.xml b/res/color/overview_scrim_foreground_primary_dark.xml similarity index 76% rename from res/color-night-v31/material_color_surface_bright.xml rename to res/color/overview_scrim_foreground_primary_dark.xml index f34ed6c548..2b46edc191 100644 --- a/res/color-night-v31/material_color_surface_bright.xml +++ b/res/color/overview_scrim_foreground_primary_dark.xml @@ -1,6 +1,5 @@ - - - - \ No newline at end of file + + diff --git a/res/color-night-v31/material_color_surface_container.xml b/res/color/overview_scrim_foreground_secondary.xml similarity index 75% rename from res/color-night-v31/material_color_surface_container.xml rename to res/color/overview_scrim_foreground_secondary.xml index 002b88eba4..f2555ccc0e 100644 --- a/res/color-night-v31/material_color_surface_container.xml +++ b/res/color/overview_scrim_foreground_secondary.xml @@ -1,6 +1,5 @@ - - - - \ No newline at end of file + + diff --git a/res/color/overview_scrim_foreground_secondary_dark.xml b/res/color/overview_scrim_foreground_secondary_dark.xml new file mode 100644 index 0000000000..79e8d8b4ec --- /dev/null +++ b/res/color/overview_scrim_foreground_secondary_dark.xml @@ -0,0 +1,18 @@ + + + + diff --git a/res/color/popup_shade_first.xml b/res/color/popup_shade_first.xml index 4b50cba3c2..9a71caeb51 100644 --- a/res/color/popup_shade_first.xml +++ b/res/color/popup_shade_first.xml @@ -13,7 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + + diff --git a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml b/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml deleted file mode 100644 index 47f2a5d73a..0000000000 --- a/res/drawable-sw720dp/ic_transient_taskbar_all_apps_button.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml index e279fa051a..39af989a1d 100644 --- a/res/drawable/add_item_dialog_background.xml +++ b/res/drawable/add_item_dialog_background.xml @@ -1,7 +1,7 @@ - + diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml index 7b27273f28..d200b9f961 100644 --- a/res/drawable/all_apps_tabs_background.xml +++ b/res/drawable/all_apps_tabs_background.xml @@ -13,36 +13,25 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - + - - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_selected.xml b/res/drawable/all_apps_tabs_background_selected.xml new file mode 100644 index 0000000000..f7873dab33 --- /dev/null +++ b/res/drawable/all_apps_tabs_background_selected.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_selected_focused.xml b/res/drawable/all_apps_tabs_background_selected_focused.xml new file mode 100644 index 0000000000..28402627ff --- /dev/null +++ b/res/drawable/all_apps_tabs_background_selected_focused.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_unselected.xml b/res/drawable/all_apps_tabs_background_unselected.xml new file mode 100644 index 0000000000..6c0adf13a5 --- /dev/null +++ b/res/drawable/all_apps_tabs_background_unselected.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/all_apps_tabs_background_unselected_focused.xml b/res/drawable/all_apps_tabs_background_unselected_focused.xml new file mode 100644 index 0000000000..83038926ba --- /dev/null +++ b/res/drawable/all_apps_tabs_background_unselected_focused.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_letter_list_text.xml b/res/drawable/bg_letter_list_text.xml new file mode 100644 index 0000000000..073730c705 --- /dev/null +++ b/res/drawable/bg_letter_list_text.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_ps_lock_button.xml b/res/drawable/bg_ps_lock_button.xml index aef1e816ef..7a20e49b22 100644 --- a/res/drawable/bg_ps_lock_button.xml +++ b/res/drawable/bg_ps_lock_button.xml @@ -21,12 +21,12 @@ android:viewportHeight="36"> + android:fillColor="@color/materialColorPrimaryFixedDim"/> + android:fillColor="@color/materialColorOnPrimaryFixed"/> \ No newline at end of file diff --git a/res/drawable/bg_ps_mask_left_corner.xml b/res/drawable/bg_ps_mask_left_corner.xml index 43eeedb0b4..f87c497751 100644 --- a/res/drawable/bg_ps_mask_left_corner.xml +++ b/res/drawable/bg_ps_mask_left_corner.xml @@ -24,7 +24,7 @@ + android:fillColor="@android:color/white" /> \ No newline at end of file diff --git a/res/drawable/bg_ps_mask_right_corner.xml b/res/drawable/bg_ps_mask_right_corner.xml index d63b866f9c..7a4c0d80c6 100644 --- a/res/drawable/bg_ps_mask_right_corner.xml +++ b/res/drawable/bg_ps_mask_right_corner.xml @@ -24,7 +24,7 @@ + android:fillColor="@android:color/white" /> \ No newline at end of file diff --git a/res/drawable/bg_ps_transition_image.xml b/res/drawable/bg_ps_transition_image.xml index dfad3cf5b3..694303c517 100644 --- a/res/drawable/bg_ps_transition_image.xml +++ b/res/drawable/bg_ps_transition_image.xml @@ -23,13 +23,13 @@ + android:fillColor="@color/materialColorOnPrimaryFixed"/> + android:fillColor="@color/materialColorPrimaryFixedDim"/> + android:fillColor="@color/materialColorOnPrimaryFixed"/> \ No newline at end of file diff --git a/res/drawable/bg_ps_unlock_button.xml b/res/drawable/bg_ps_unlock_button.xml index d5eedd293e..563c3f6d68 100644 --- a/res/drawable/bg_ps_unlock_button.xml +++ b/res/drawable/bg_ps_unlock_button.xml @@ -21,9 +21,9 @@ android:viewportHeight="36"> + android:fillColor="@color/materialColorPrimaryFixedDim"/> \ No newline at end of file diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml index ca9448964b..bf5d842577 100644 --- a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml +++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml @@ -15,8 +15,7 @@ --> - + diff --git a/res/drawable/bg_widgets_header_states_two_pane.xml b/res/drawable/bg_widgets_header_states_two_pane.xml index 5f4b8c66b4..1ec41a9a0a 100644 --- a/res/drawable/bg_widgets_header_states_two_pane.xml +++ b/res/drawable/bg_widgets_header_states_two_pane.xml @@ -14,18 +14,16 @@ limitations under the License. --> - - - - - - + + + + - - - - - - + + + + + + diff --git a/res/drawable/bg_widgets_header_two_pane.xml b/res/drawable/bg_widgets_header_two_pane.xml index ca3feef1f0..e237002898 100644 --- a/res/drawable/bg_widgets_header_two_pane.xml +++ b/res/drawable/bg_widgets_header_two_pane.xml @@ -14,13 +14,10 @@ limitations under the License. --> - - + android:insetTop="@dimen/widget_list_entry_spacing"> + - - + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml new file mode 100644 index 0000000000..0ee3d14715 --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml new file mode 100644 index 0000000000..9028ebe270 --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml new file mode 100644 index 0000000000..12dc907bd3 --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml new file mode 100644 index 0000000000..ba26f9fc3e --- /dev/null +++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml index f5b68866cb..723668fb39 100644 --- a/res/drawable/button_top_rounded_bordered_ripple.xml +++ b/res/drawable/button_top_rounded_bordered_ripple.xml @@ -25,7 +25,7 @@ android:topRightRadius="12dp" android:bottomLeftRadius="4dp" android:bottomRightRadius="4dp" /> - + diff --git a/res/drawable/cloud_download_24px.xml b/res/drawable/cloud_download_24px.xml new file mode 100644 index 0000000000..6f7c95aac2 --- /dev/null +++ b/res/drawable/cloud_download_24px.xml @@ -0,0 +1,11 @@ + + + + diff --git a/res/drawable/cloud_download_semibold_24px.xml b/res/drawable/cloud_download_semibold_24px.xml new file mode 100644 index 0000000000..ef15f9f735 --- /dev/null +++ b/res/drawable/cloud_download_semibold_24px.xml @@ -0,0 +1,11 @@ + + + + diff --git a/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml b/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml new file mode 100644 index 0000000000..b96a596e94 --- /dev/null +++ b/res/drawable/desktop_mode_ic_taskbar_menu_new_window.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/hourglass_24px.xml b/res/drawable/hourglass_24px.xml new file mode 100644 index 0000000000..9ec5df301e --- /dev/null +++ b/res/drawable/hourglass_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/drawable/ic_aspect_ratio.xml b/res/drawable/ic_aspect_ratio.xml new file mode 100644 index 0000000000..aafaac4618 --- /dev/null +++ b/res/drawable/ic_aspect_ratio.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_bubble_button.xml b/res/drawable/ic_bubble_button.xml new file mode 100644 index 0000000000..143c5c93fe --- /dev/null +++ b/res/drawable/ic_bubble_button.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/quickstep/res/drawable/bg_overview_clear_all_button.xml b/res/drawable/ic_chevron_end.xml similarity index 60% rename from quickstep/res/drawable/bg_overview_clear_all_button.xml rename to res/drawable/ic_chevron_end.xml index 3b83092e48..9ca4f3a3b4 100644 --- a/quickstep/res/drawable/bg_overview_clear_all_button.xml +++ b/res/drawable/ic_chevron_end.xml @@ -1,6 +1,5 @@ - - - - - - - - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:autoMirrored="true" + android:tint="?attr/colorControlNormal"> + + diff --git a/res/drawable/ic_chevron_left_rounded_700.xml b/res/drawable/ic_chevron_left_rounded_700.xml new file mode 100644 index 0000000000..c3730d3a57 --- /dev/null +++ b/res/drawable/ic_chevron_left_rounded_700.xml @@ -0,0 +1,11 @@ + + + diff --git a/res/drawable/ic_chevron_start.xml b/res/drawable/ic_chevron_start.xml new file mode 100644 index 0000000000..913da026da --- /dev/null +++ b/res/drawable/ic_chevron_start.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/res/drawable/ic_circle.xml b/res/drawable/ic_circle.xml new file mode 100644 index 0000000000..2f5cf218e1 --- /dev/null +++ b/res/drawable/ic_circle.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml new file mode 100644 index 0000000000..24f61dd543 --- /dev/null +++ b/res/drawable/ic_close_work_edu.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/ic_corp_off.xml b/res/drawable/ic_corp_off.xml index 117258e3bd..e58e1729cf 100644 --- a/res/drawable/ic_corp_off.xml +++ b/res/drawable/ic_corp_off.xml @@ -16,9 +16,9 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?android:attr/textColorHint"> + android:viewportHeight="24"> - \ No newline at end of file + android:pathData="M16,6H20C21.11,6 22,6.89 22,8V18.99C22,19.021 21.994,19.05 21.989,19.077C21.984,19.102 21.98,19.126 21.98,19.15L20,17.17V8H10.83L8,5.17V4C8,2.89 8.89,2 10,2H14C15.11,2 16,2.89 16,4V6ZM10,6H14V4H10V6ZM19,19L8,8L6,6L2.81,2.81L1.39,4.22L3.3,6.13C2.54,6.41 2.01,7.14 2.01,8L2,19C2,20.11 2.89,21 4,21H18.17L19.78,22.61L21.19,21.2L20.82,20.83L19,19ZM4,8V19H16.17L5.17,8H4Z" + android:fillColor="@color/materialColorOnPrimary" + android:fillType="evenOdd"/> + diff --git a/res/drawable/ic_desktop_add.xml b/res/drawable/ic_desktop_add.xml new file mode 100644 index 0000000000..d31b04b858 --- /dev/null +++ b/res/drawable/ic_desktop_add.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/res/drawable/ic_desktop_with_bg.xml b/res/drawable/ic_desktop_with_bg.xml new file mode 100644 index 0000000000..f54285c444 --- /dev/null +++ b/res/drawable/ic_desktop_with_bg.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/res/drawable/ic_game.xml b/res/drawable/ic_game.xml new file mode 100644 index 0000000000..552419fb15 --- /dev/null +++ b/res/drawable/ic_game.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_info_no_shadow.xml b/res/drawable/ic_info_no_shadow.xml index 29a81bd27c..31cf51200a 100644 --- a/res/drawable/ic_info_no_shadow.xml +++ b/res/drawable/ic_info_no_shadow.xml @@ -18,7 +18,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/materialColorOnSurface"> + android:tint="@color/materialColorOnSurface"> + android:tint="@color/materialColorOnSurface"> + android:tint="@color/materialColorOnSurface"> diff --git a/res/drawable/ic_more_horiz_24.xml b/res/drawable/ic_more_horiz_24.xml new file mode 100644 index 0000000000..d46827cede --- /dev/null +++ b/res/drawable/ic_more_horiz_24.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_more_vert_dots.xml b/res/drawable/ic_more_vert_dots.xml new file mode 100644 index 0000000000..c4659f821f --- /dev/null +++ b/res/drawable/ic_more_vert_dots.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_private_profile_divider_badge.xml b/res/drawable/ic_private_profile_divider_badge.xml new file mode 100644 index 0000000000..92292f6bd7 --- /dev/null +++ b/res/drawable/ic_private_profile_divider_badge.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml b/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml new file mode 100644 index 0000000000..8d125988c1 --- /dev/null +++ b/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/res/drawable/ic_private_space_with_background.xml b/res/drawable/ic_private_space_with_background.xml index fe85168d3b..d66549d90d 100644 --- a/res/drawable/ic_private_space_with_background.xml +++ b/res/drawable/ic_private_space_with_background.xml @@ -13,20 +13,19 @@ limitations under the License. --> + android:fillColor="@color/materialColorSurfaceContainerLowest" /> + android:fillColor="@color/materialColorOnSurface" /> + android:fillColor="@color/materialColorOnSurface" /> diff --git a/res/drawable/ic_ps_settings.xml b/res/drawable/ic_ps_settings.xml index 47edeb85ef..5453f35776 100644 --- a/res/drawable/ic_ps_settings.xml +++ b/res/drawable/ic_ps_settings.xml @@ -24,9 +24,9 @@ android:pathData="M10,10h20v20h-20z"/> + android:fillColor="@color/materialColorOnSurfaceVariant"/> + android:fillColor="@color/materialColorOnSurfaceVariant"/> \ No newline at end of file diff --git a/res/drawable/ic_schedule.xml b/res/drawable/ic_schedule.xml new file mode 100644 index 0000000000..d57b0a7806 --- /dev/null +++ b/res/drawable/ic_schedule.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_split_horizontal.xml b/res/drawable/ic_split_horizontal.xml index 2efd2b9bc2..26efedc4d9 100644 --- a/res/drawable/ic_split_horizontal.xml +++ b/res/drawable/ic_split_horizontal.xml @@ -1,9 +1,25 @@ + - + android:width="20dp" + android:height="20dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + diff --git a/res/drawable/ic_split_vertical.xml b/res/drawable/ic_split_vertical.xml index 9bc97851ab..787953a52f 100644 --- a/res/drawable/ic_split_vertical.xml +++ b/res/drawable/ic_split_vertical.xml @@ -1,9 +1,25 @@ + - + android:width="20dp" + android:height="20dp" + android:tint="?attr/colorControlNormal" + android:viewportHeight="960" + android:viewportWidth="960"> + diff --git a/res/drawable/ic_taskbar_all_apps_button.xml b/res/drawable/ic_taskbar_all_apps_button.xml deleted file mode 100644 index 82fbbea617..0000000000 --- a/res/drawable/ic_taskbar_all_apps_button.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/drawable/ic_taskbar_all_apps_search_button_expressive_theme.xml b/res/drawable/ic_taskbar_all_apps_search_button_expressive_theme.xml new file mode 100644 index 0000000000..852a077221 --- /dev/null +++ b/res/drawable/ic_taskbar_all_apps_search_button_expressive_theme.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_taskbar_minimal_state_all_apps_search_button_expressive_theme.xml b/res/drawable/ic_taskbar_minimal_state_all_apps_search_button_expressive_theme.xml new file mode 100644 index 0000000000..ed4a821c79 --- /dev/null +++ b/res/drawable/ic_taskbar_minimal_state_all_apps_search_button_expressive_theme.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_transient_taskbar_all_apps_button.xml b/res/drawable/ic_transient_taskbar_all_apps_button.xml deleted file mode 100644 index 6e740aed4f..0000000000 --- a/res/drawable/ic_transient_taskbar_all_apps_button.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/drawable/ic_translate.xml b/res/drawable/ic_translate.xml new file mode 100644 index 0000000000..add47bee8d --- /dev/null +++ b/res/drawable/ic_translate.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/res/drawable/ic_uninstall_no_shadow.xml b/res/drawable/ic_uninstall_no_shadow.xml index 6200054f1b..829e590780 100644 --- a/res/drawable/ic_uninstall_no_shadow.xml +++ b/res/drawable/ic_uninstall_no_shadow.xml @@ -18,7 +18,7 @@ android:height="20dp" android:viewportWidth="24.0" android:viewportHeight="24.0" - android:tint="?attr/materialColorOnSurface" > + android:tint="@color/materialColorOnSurface" > diff --git a/res/drawable/ic_unpin.xml b/res/drawable/ic_unpin.xml new file mode 100644 index 0000000000..557b4f9d76 --- /dev/null +++ b/res/drawable/ic_unpin.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/res/drawable/ic_view_carousel.xml b/res/drawable/ic_view_carousel.xml new file mode 100644 index 0000000000..53d730abbb --- /dev/null +++ b/res/drawable/ic_view_carousel.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/res/drawable/ic_visibility_filled.xml b/res/drawable/ic_visibility_filled.xml new file mode 100644 index 0000000000..1e788c9b50 --- /dev/null +++ b/res/drawable/ic_visibility_filled.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/res/drawable/icon_menu_arrow_background.xml b/res/drawable/icon_menu_arrow_background.xml index 8af3c00fad..1de111ab29 100644 --- a/res/drawable/icon_menu_arrow_background.xml +++ b/res/drawable/icon_menu_arrow_background.xml @@ -15,14 +15,13 @@ limitations under the License. --> + android:centerColor="@color/materialColorSurfaceBright" + android:endColor="@color/materialColorSurfaceBright" /> \ No newline at end of file diff --git a/res/drawable/info_24px.xml b/res/drawable/info_24px.xml new file mode 100644 index 0000000000..38417d29c3 --- /dev/null +++ b/res/drawable/info_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/drawable/inset_rounded_action_button.xml b/res/drawable/inset_rounded_action_button.xml new file mode 100644 index 0000000000..7b4c4d03c0 --- /dev/null +++ b/res/drawable/inset_rounded_action_button.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/res/drawable/keep_24px.xml b/res/drawable/keep_24px.xml new file mode 100644 index 0000000000..ff9bff64bf --- /dev/null +++ b/res/drawable/keep_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/drawable/popup_background.xml b/res/drawable/popup_background.xml index 6eedecb6ca..686456f7ab 100644 --- a/res/drawable/popup_background.xml +++ b/res/drawable/popup_background.xml @@ -15,6 +15,6 @@ --> - + \ No newline at end of file diff --git a/res/drawable/private_space_app_divider.xml b/res/drawable/private_space_app_divider.xml index 1ea12b3328..81cb4017b0 100644 --- a/res/drawable/private_space_app_divider.xml +++ b/res/drawable/private_space_app_divider.xml @@ -16,6 +16,6 @@ - + \ No newline at end of file diff --git a/res/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml index cfec2b126d..1e7fe43527 100644 --- a/res/drawable/private_space_install_app_icon.xml +++ b/res/drawable/private_space_install_app_icon.xml @@ -13,19 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - - - - - - + + + + diff --git a/res/drawable/private_space_install_app_icon_foreground.xml b/res/drawable/private_space_install_app_icon_foreground.xml new file mode 100644 index 0000000000..d55abe7304 --- /dev/null +++ b/res/drawable/private_space_install_app_icon_foreground.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/drawable/ps_lock_background.xml b/res/drawable/ps_lock_background.xml index 0be83dbd92..bc66595ac3 100644 --- a/res/drawable/ps_lock_background.xml +++ b/res/drawable/ps_lock_background.xml @@ -21,7 +21,7 @@ - + diff --git a/res/drawable/ps_settings_background.xml b/res/drawable/ps_settings_background.xml index b0c6b5b0d1..7d568dc68e 100644 --- a/res/drawable/ps_settings_background.xml +++ b/res/drawable/ps_settings_background.xml @@ -18,6 +18,6 @@ android:inset="4dp"> - + \ No newline at end of file diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml index 9deab6eb57..6ee6d65d1f 100644 --- a/res/drawable/rounded_action_button.xml +++ b/res/drawable/rounded_action_button.xml @@ -7,7 +7,7 @@ ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ - ~ Unless required by applicable law or agreed to in writing, software + ~ Unless required by applicable law or agreed to in writing, soft]ware ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and @@ -16,15 +16,11 @@ - + - + android:color="@color/materialColorSurfaceContainerLow" /> diff --git a/res/drawable/splitscreen_24px.xml b/res/drawable/splitscreen_24px.xml new file mode 100644 index 0000000000..3485251abf --- /dev/null +++ b/res/drawable/splitscreen_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/drawable/widget_picker_tabs_background.xml b/res/drawable/widget_picker_tabs_background.xml index a874dd8b90..f6607b7ad1 100644 --- a/res/drawable/widget_picker_tabs_background.xml +++ b/res/drawable/widget_picker_tabs_background.xml @@ -13,36 +13,39 @@ See the License for the specific language governing permissions and limitations under the License. --> - + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/widgets_24px.xml b/res/drawable/widgets_24px.xml new file mode 100644 index 0000000000..4f4358d073 --- /dev/null +++ b/res/drawable/widgets_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/drawable/widgets_list_expand_button_background.xml b/res/drawable/widgets_list_expand_button_background.xml new file mode 100644 index 0000000000..068b26db43 --- /dev/null +++ b/res/drawable/widgets_list_expand_button_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml index 1437c6f7d7..ff66971af0 100644 --- a/res/drawable/work_card.xml +++ b/res/drawable/work_card.xml @@ -17,6 +17,7 @@ - + + diff --git a/res/drawable/work_mode_fab_background.xml b/res/drawable/work_mode_fab_background.xml index 6be33e86ce..ad795eb478 100644 --- a/res/drawable/work_mode_fab_background.xml +++ b/res/drawable/work_mode_fab_background.xml @@ -18,7 +18,10 @@ - + + diff --git a/res/drawable/bg_ps_header.xml b/res/drawable/work_scheduler_background.xml similarity index 65% rename from res/drawable/bg_ps_header.xml rename to res/drawable/work_scheduler_background.xml index da314452ad..50c81837d8 100644 --- a/res/drawable/bg_ps_header.xml +++ b/res/drawable/work_scheduler_background.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/res/layout/fast_scroller_letter_list_text_view.xml b/res/layout/fast_scroller_letter_list_text_view.xml new file mode 100644 index 0000000000..493b6fcb7e --- /dev/null +++ b/res/layout/fast_scroller_letter_list_text_view.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml index fe06f455c2..0b3385238b 100644 --- a/res/layout/launcher.xml +++ b/res/layout/launcher.xml @@ -29,6 +29,7 @@ android:importantForAccessibility="no"> - - - + android:id="@+id/ps_header_layout" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="@dimen/ps_header_height" + android:background="@android:color/transparent" + android:clipToOutline="true" + android:gravity="center_vertical" + android:textDirection="locale" + android:orientation="horizontal" + android:contentDescription="@string/ps_container_lock_button_content_description" + android:importantForAccessibility="yes"> + android:alpha="0" + style="@style/PrivateSpaceLockTextStyle"/> - \ No newline at end of file + diff --git a/res/layout/private_space_mask_view.xml b/res/layout/private_space_mask_view.xml index 44e2797020..ad8947f20e 100644 --- a/res/layout/private_space_mask_view.xml +++ b/res/layout/private_space_mask_view.xml @@ -50,6 +50,6 @@ app:layout_constraintEnd_toEndOf="@id/right_corner" app:layout_constraintTop_toBottomOf="@id/left_corner" android:importantForAccessibility="no" - android:background="?attr/allAppsScrimColor"/> + android:background="@android:color/white"/> \ No newline at end of file diff --git a/res/color-night-v31/material_color_surface_variant.xml b/res/layout/qsb_container_hotseat.xml similarity index 74% rename from res/color-night-v31/material_color_surface_variant.xml rename to res/layout/qsb_container_hotseat.xml index a645f24dd9..2fd3db1e78 100644 --- a/res/color-night-v31/material_color_surface_variant.xml +++ b/res/layout/qsb_container_hotseat.xml @@ -1,6 +1,6 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml index 43a8aac8b2..afddc44a78 100644 --- a/res/layout/user_folder_icon_normalized.xml +++ b/res/layout/user_folder_icon_normalized.xml @@ -40,12 +40,11 @@ + + + diff --git a/quickstep/res/layout/widget_picker_activity.xml b/res/layout/widget_picker_activity.xml similarity index 100% rename from quickstep/res/layout/widget_picker_activity.xml rename to res/layout/widget_picker_activity.xml diff --git a/res/layout/widget_recommendations.xml b/res/layout/widget_recommendations.xml index 5879b0f1a9..545362940d 100644 --- a/res/layout/widget_recommendations.xml +++ b/res/layout/widget_recommendations.xml @@ -22,6 +22,7 @@ --> - + android:layout_height="match_parent"> - - \ No newline at end of file + + diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml index a292d67cc2..21f2c287c0 100644 --- a/res/layout/widgets_full_sheet_paged_view.xml +++ b/res/layout/widgets_full_sheet_paged_view.xml @@ -53,6 +53,7 @@ @@ -103,7 +105,6 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding" - android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding" android:layout_weight="1" android:background="@drawable/widget_picker_tabs_background" android:text="@string/widgets_full_sheet_personal_tab" @@ -116,7 +117,6 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding" - android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding" android:layout_weight="1" android:background="@drawable/widget_picker_tabs_background" android:text="@string/widgets_full_sheet_work_tab" diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml index 5427732c4d..7ef4c25465 100644 --- a/res/layout/widgets_full_sheet_recyclerview.xml +++ b/res/layout/widgets_full_sheet_recyclerview.xml @@ -36,6 +36,7 @@ diff --git a/res/layout/widgets_list_expand_button.xml b/res/layout/widgets_list_expand_button.xml new file mode 100644 index 0000000000..ff2d777a0f --- /dev/null +++ b/res/layout/widgets_list_expand_button.xml @@ -0,0 +1,34 @@ + + +